@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
@@ -25,7 +25,7 @@ import {
25
25
  SupportedChainId,
26
26
  } from "@/util/constants";
27
27
  import { useMemo } from "react";
28
- import { EXCHANGE_ICON_BY_TYPE } from "@/components/steps/ExchangeFlow";
28
+ import { EXCHANGE_ICON_BY_TYPE } from "@/components/steps/shared/exchangeIntegration";
29
29
 
30
30
  export const ERROR_MSG =
31
31
  "Swap not found for a required underlying of defi route, please make sure your amount is within an acceptable range";
@@ -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,412 @@
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 [selectedQuote, setSelectedQuote] = useState<MeldQuote | null>(null);
84
+ const [error, setError] = useState<string | null>(null);
85
+
86
+ // Smart account address - use recipient override if provided
87
+ const { smartAccountAddress } = useSmartAccountAddress(chainIdOut);
88
+
89
+ // Country code detection
90
+ const { countryCode, isLoading: countryCodeLoading } = useCountryCode();
91
+ const { data: supportedCryptos = [], isLoading: supportedCryptosLoading } =
92
+ useMeldSupportedCrypto({
93
+ countryCode,
94
+ enabled: !!chainIdOut,
95
+ });
96
+
97
+ const destinationSymbol = "USDC";
98
+
99
+ const destinationCryptoCode = useMemo(() => {
100
+ if (!chainIdOut) return null;
101
+ return getMeldDestinationCurrencyCode({
102
+ symbol: destinationSymbol,
103
+ chainId: chainIdOut,
104
+ supportedCryptos,
105
+ });
106
+ }, [destinationSymbol, chainIdOut, supportedCryptos]);
107
+
108
+ const isCardBuySupported =
109
+ !!chainIdOut &&
110
+ isMeldCardBuySupportedChain(chainIdOut, supportedCryptos);
111
+
112
+ // Fetch quotes
113
+ const {
114
+ data: quotes,
115
+ isLoading: quotesLoading,
116
+ error: quotesError,
117
+ } = useMeldQuotes({
118
+ sourceCurrency: fiatCurrency,
119
+ destinationCurrency: destinationCryptoCode || "",
120
+ amount: parseFloat(fiatAmount) || 0,
121
+ countryCode: countryCode || "",
122
+ walletAddress: smartAccountAddress || "",
123
+ enabled:
124
+ !!countryCode &&
125
+ !!destinationCryptoCode &&
126
+ parseFloat(fiatAmount) > 0 &&
127
+ !!smartAccountAddress,
128
+ });
129
+
130
+ // Select best quote automatically
131
+ useEffect(() => {
132
+ if (!quotes || quotes.length === 0) {
133
+ if (selectedQuote) setSelectedQuote(null);
134
+ return;
135
+ }
136
+
137
+ // Default to best quote
138
+ if (!selectedQuote) {
139
+ setSelectedQuote(quotes[0]);
140
+ return;
141
+ }
142
+
143
+ // Keep the same provider selected if still available, but refresh the
144
+ // quote object (rates/fees can update on refetch).
145
+ const matching = quotes.find(
146
+ (q) => q.serviceProvider === selectedQuote.serviceProvider,
147
+ );
148
+ if (!matching) {
149
+ setSelectedQuote(quotes[0]);
150
+ return;
151
+ }
152
+
153
+ const didChange =
154
+ matching.destinationAmount !== selectedQuote.destinationAmount ||
155
+ matching.totalFee !== selectedQuote.totalFee ||
156
+ matching.exchangeRate !== selectedQuote.exchangeRate;
157
+ if (didChange) setSelectedQuote(matching);
158
+ }, [quotes, selectedQuote]);
159
+
160
+ // MELD flow state
161
+ const meldFlow = useMeldCardBuyFlow();
162
+
163
+ const shouldTrackIncomingTransfer =
164
+ currentStep === CardBuyStep.OpenWidget &&
165
+ !!chainIdOut &&
166
+ !!smartAccountAddress;
167
+ const {
168
+ detectedAmount,
169
+ isDetected,
170
+ reset: resetIncomingDetector,
171
+ } = useIncomingUsdcTransferDetector({
172
+ chainId: chainIdOut,
173
+ walletAddress: smartAccountAddress,
174
+ enabled: shouldTrackIncomingTransfer,
175
+ thresholdUsdc: "0.01",
176
+ });
177
+
178
+ const resetToChooseAmount = useCallback(
179
+ (clearQuote = false) => {
180
+ meldFlow.reset();
181
+ resetIncomingDetector();
182
+ handoffTriggeredRef.current = false;
183
+ setError(null);
184
+ if (clearQuote) {
185
+ setSelectedQuote(null);
186
+ }
187
+ setCurrentStep(CardBuyStep.ChooseAmount);
188
+ },
189
+ [meldFlow, resetIncomingDetector],
190
+ );
191
+
192
+ // Navigation
193
+ const goBack = useCallback(() => {
194
+ if (currentStep === CardBuyStep.ChooseAmount) {
195
+ setFlow("");
196
+ return;
197
+ }
198
+ const previousStep = cardPreviousStep[currentStep];
199
+ if (previousStep !== undefined) {
200
+ resetToChooseAmount(false);
201
+ return;
202
+ }
203
+ setFlow("");
204
+ }, [currentStep, setFlow, resetToChooseAmount]);
205
+
206
+ const goToOpenWidget = useCallback(() => {
207
+ if (!selectedQuote) return;
208
+ setError(null);
209
+ handoffTriggeredRef.current = false;
210
+ resetIncomingDetector();
211
+ setCurrentStep(CardBuyStep.OpenWidget);
212
+ }, [selectedQuote, resetIncomingDetector]);
213
+
214
+ // Start MELD session
215
+ const handleStartSession = useCallback(async () => {
216
+ if (
217
+ !selectedQuote ||
218
+ !smartAccountAddress ||
219
+ !destinationCryptoCode ||
220
+ !countryCode
221
+ )
222
+ return;
223
+
224
+ try {
225
+ await meldFlow.startSession({
226
+ countryCode: countryCode,
227
+ sourceCurrencyCode: fiatCurrency,
228
+ destinationCurrencyCode: destinationCryptoCode,
229
+ sourceAmount: selectedQuote.sourceAmount,
230
+ walletAddress: smartAccountAddress,
231
+ serviceProvider: selectedQuote.serviceProvider,
232
+ paymentMethodType: "CREDIT_DEBIT_CARD",
233
+ externalCustomerId: address,
234
+ externalSessionId: `enso-${Date.now()}`,
235
+ });
236
+ } catch (err: any) {
237
+ console.error(err);
238
+ setError(err.message || "Failed to create session");
239
+ }
240
+ }, [
241
+ selectedQuote,
242
+ smartAccountAddress,
243
+ destinationCryptoCode,
244
+ countryCode,
245
+ fiatCurrency,
246
+ address,
247
+ meldFlow,
248
+ ]);
249
+
250
+ const handleOpenWidget = useCallback(() => {
251
+ meldFlow.openWidget();
252
+ }, [meldFlow]);
253
+
254
+ // Funds arrival is the source of truth for handing off into Smart Account flow.
255
+ useEffect(() => {
256
+ if (currentStep !== CardBuyStep.OpenWidget) return;
257
+ if (!isDetected || !detectedAmount || !chainIdOut) return;
258
+ if (handoffTriggeredRef.current) return;
259
+
260
+ try {
261
+ handoffTriggeredRef.current = true;
262
+ setError(null);
263
+ resetIncomingDetector();
264
+
265
+ // Store the detected amount and token before switching flow
266
+ setAmountIn(denormalizeValue(detectedAmount, 6));
267
+ setTokenIn(getUsdcAddress(chainIdOut));
268
+ setChainIdIn(chainIdOut);
269
+
270
+ onCardBuyDepositDetected();
271
+ } catch (err: any) {
272
+ handoffTriggeredRef.current = false;
273
+ setError(
274
+ err.message || "Failed to redirect to Smart Account quote",
275
+ );
276
+ }
277
+ }, [
278
+ currentStep,
279
+ isDetected,
280
+ detectedAmount,
281
+ chainIdOut,
282
+ setAmountIn,
283
+ setTokenIn,
284
+ setChainIdIn,
285
+ onCardBuyDepositDetected,
286
+ resetIncomingDetector,
287
+ ]);
288
+
289
+ // Check if chain is supported
290
+ if (!chainIdOut || (!supportedCryptosLoading && !isCardBuySupported)) {
291
+ return (
292
+ <>
293
+ <Modal.Header>
294
+ <HeaderWrapper>
295
+ <IconButton onClick={() => setFlow("")} width="40px">
296
+ <Icon
297
+ as={ChevronLeft}
298
+ color="fg.muted"
299
+ width="16px"
300
+ height="16px"
301
+ />
302
+ </IconButton>
303
+ <Box>
304
+ <HeaderTitle>Buy with Card</HeaderTitle>
305
+ </Box>
306
+ {handleClose && (
307
+ <IconButton onClick={handleClose} width="40px">
308
+ <Icon
309
+ as={X}
310
+ color="fg.muted"
311
+ width="16px"
312
+ height="16px"
313
+ />
314
+ </IconButton>
315
+ )}
316
+ </HeaderWrapper>
317
+ </Modal.Header>
318
+ <Modal.Body>
319
+ <BodyWrapper>
320
+ <Center py={8} flexDirection="column" gap={4}>
321
+ <Icon as={XCircle} color="error" boxSize={12} />
322
+ <Text color="fg.muted" textAlign="center">
323
+ Card purchases are not supported for this chain.
324
+ </Text>
325
+ </Center>
326
+ </BodyWrapper>
327
+ </Modal.Body>
328
+ </>
329
+ );
330
+ }
331
+
332
+ // Render step content
333
+ const renderStepContent = () => {
334
+ switch (currentStep) {
335
+ case CardBuyStep.ChooseAmount:
336
+ return (
337
+ <ChooseAmountStep
338
+ fiatCurrency={fiatCurrency}
339
+ setFiatCurrency={setFiatCurrency}
340
+ fiatAmount={fiatAmount}
341
+ setFiatAmount={setFiatAmount}
342
+ quotes={quotes}
343
+ quotesLoading={quotesLoading || countryCodeLoading}
344
+ quotesError={
345
+ quotesError
346
+ ? quotesError.message || String(quotesError)
347
+ : null
348
+ }
349
+ selectedQuote={selectedQuote}
350
+ setSelectedQuote={setSelectedQuote}
351
+ destinationSymbol={destinationSymbol}
352
+ onContinue={goToOpenWidget}
353
+ />
354
+ );
355
+
356
+ case CardBuyStep.OpenWidget:
357
+ return (
358
+ <OpenWidgetStep
359
+ selectedQuote={selectedQuote!}
360
+ isCreatingSession={meldFlow.isCreatingSession}
361
+ widgetUrl={meldFlow.widgetUrl}
362
+ sessionId={meldFlow.sessionId}
363
+ onCreateSession={handleStartSession}
364
+ onOpenWidget={handleOpenWidget}
365
+ isTransferDetected={isDetected}
366
+ detectedAmount={detectedAmount}
367
+ error={error || meldFlow.sessionError?.message}
368
+ />
369
+ );
370
+
371
+ default:
372
+ return null;
373
+ }
374
+ };
375
+
376
+ return (
377
+ <>
378
+ <Modal.Header>
379
+ <HeaderWrapper>
380
+ <IconButton onClick={goBack} width="40px">
381
+ <Icon
382
+ as={ChevronLeft}
383
+ color="fg.muted"
384
+ width="16px"
385
+ height="16px"
386
+ />
387
+ </IconButton>
388
+ <Box>
389
+ <HeaderTitle>{STEP_TITLES[currentStep]}</HeaderTitle>
390
+ </Box>
391
+ {handleClose && (
392
+ <IconButton onClick={handleClose} width="40px">
393
+ <Icon
394
+ as={X}
395
+ color="fg.muted"
396
+ width="16px"
397
+ height="16px"
398
+ />
399
+ </IconButton>
400
+ )}
401
+ </HeaderWrapper>
402
+ </Modal.Header>
403
+ <Modal.Body>
404
+ <AnimatedStep key={currentStep}>
405
+ {renderStepContent()}
406
+ </AnimatedStep>
407
+ </Modal.Body>
408
+ </>
409
+ );
410
+ };
411
+
412
+ export default CardBuyFlow;