@ensofinance/checkout-widget 0.0.19 → 0.1.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.
@@ -1,778 +0,0 @@
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 { ChevronLeft, X, CreditCard, ExternalLink, CheckCircle, XCircle } from "lucide-react";
12
- import { useContext, useEffect, useMemo, useState, useCallback } from "react";
13
- import { useAccount, useSignMessage } from "wagmi";
14
- import { getUserOperationHash } from "viem/account-abstraction";
15
- import {
16
- BodyWrapper,
17
- HeaderDescription,
18
- HeaderTitle,
19
- HeaderWrapper,
20
- ListWrapper,
21
- } from "../ui/styled";
22
- import { IconButton, Button, Input } from "../ui";
23
- import { CheckoutContext } from "../Checkout";
24
- import Modal from "../modal";
25
- import { useAppStore } from "@/store";
26
- import {
27
- formatUSD,
28
- normalizeValue,
29
- denormalizeValue,
30
- } from "@/util";
31
- import {
32
- useAppDetails,
33
- useRouteData,
34
- useSmartAccountAddress,
35
- } from "@/util/enso-hooks";
36
- import {
37
- useMeldQuotes,
38
- useMeldCardBuyFlow,
39
- useCountryCode,
40
- getMeldCryptoCode,
41
- isMeldSupportedChain,
42
- NATIVE_TOKEN_SYMBOLS,
43
- formatProviderName,
44
- } from "@/util/meld-hooks";
45
- import { TransactionDetailRow } from "../TransactionDetailRow";
46
- import { CircleTimer } from "../CircleTimer";
47
- import type { MeldQuote } from "@/types";
48
- import { ETH_ADDRESS } from "@/util/constants";
49
-
50
- const ENTRY_POINT_ADDRESS: `0x${string}` =
51
- "0x0000000071727de22e5e9d8baf0edac6f37da032";
52
-
53
- // Card buy flow steps
54
- export enum CardBuyStep {
55
- ChooseAmount = 0,
56
- SignUserOp = 1,
57
- OpenWidget = 2,
58
- TrackPurchase = 3,
59
- TrackUserOp = 4,
60
- }
61
-
62
- const CARD_ICONS = [
63
- "https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Mastercard-logo.svg/200px-Mastercard-logo.svg.png",
64
- "https://upload.wikimedia.org/wikipedia/commons/thumb/5/5e/Visa_Inc._logo.svg/200px-Visa_Inc._logo.svg.png",
65
- ];
66
-
67
- interface CardBuyFlowProps {
68
- setFlow: (flow: string) => void;
69
- initialStep?: CardBuyStep;
70
- }
71
-
72
- const CardBuyFlow = ({ setFlow, initialStep }: CardBuyFlowProps) => {
73
- const { handleClose } = useContext(CheckoutContext);
74
- const { address } = useAccount();
75
- const { signMessageAsync } = useSignMessage();
76
-
77
- // Store
78
- const chainIdOut = useAppStore((s) => s.chainIdOut);
79
- const tokenOut = useAppStore((s) => s.tokenOut);
80
- const setAmountIn = useAppStore((s) => s.setAmountIn);
81
- const setTokenIn = useAppStore((s) => s.setTokenIn);
82
- const setChainIdIn = useAppStore((s) => s.setChainIdIn);
83
- const recipient = useAppStore((s) => s.recipient);
84
-
85
- // Local state
86
- const [currentStep, setCurrentStep] = useState<CardBuyStep>(
87
- initialStep ?? CardBuyStep.ChooseAmount
88
- );
89
- const [fiatAmount, setFiatAmount] = useState<string>("100");
90
- const [selectedQuote, setSelectedQuote] = useState<MeldQuote | null>(null);
91
- const [signedUserOp, setSignedUserOp] = useState<any>(null);
92
- const [error, setError] = useState<string | null>(null);
93
-
94
- // Smart account address - use recipient override if provided
95
- const { smartAccountAddress: derivedSmartAccountAddress } = useSmartAccountAddress(address, chainIdOut);
96
- const smartAccountAddress = recipient || derivedSmartAccountAddress;
97
-
98
- // Country code detection
99
- const { countryCode } = useCountryCode();
100
-
101
- // Get destination crypto code
102
- const destinationSymbol = useMemo(() => {
103
- // If tokenOut is native token address, use chain's native symbol
104
- if (tokenOut?.toLowerCase() === ETH_ADDRESS.toLowerCase()) {
105
- return NATIVE_TOKEN_SYMBOLS[chainIdOut!] || "ETH";
106
- }
107
- // Otherwise we'd need to look up the token symbol
108
- // For now, default to the chain's native token
109
- return NATIVE_TOKEN_SYMBOLS[chainIdOut!] || "ETH";
110
- }, [tokenOut, chainIdOut]);
111
-
112
- const destinationCryptoCode = useMemo(() => {
113
- if (!chainIdOut || !isMeldSupportedChain(chainIdOut)) return null;
114
- try {
115
- return getMeldCryptoCode(destinationSymbol, chainIdOut);
116
- } catch {
117
- return null;
118
- }
119
- }, [destinationSymbol, chainIdOut]);
120
-
121
- // Fetch quotes
122
- const {
123
- data: quotes,
124
- isLoading: quotesLoading,
125
- error: quotesError,
126
- } = useMeldQuotes({
127
- sourceCurrency: "USD",
128
- destinationCurrency: destinationCryptoCode || "",
129
- amount: parseFloat(fiatAmount) || 0,
130
- countryCode,
131
- enabled: !!destinationCryptoCode && parseFloat(fiatAmount) > 0,
132
- });
133
-
134
- // Select best quote automatically
135
- useEffect(() => {
136
- if (quotes && quotes.length > 0 && !selectedQuote) {
137
- setSelectedQuote(quotes[0]);
138
- }
139
- }, [quotes, selectedQuote]);
140
-
141
- // MELD flow state
142
- const meldFlow = useMeldCardBuyFlow();
143
-
144
- // Route data for userOp
145
- const { tokenOutData, effectiveTokenOutData } = useAppDetails();
146
- const { routeData, routeLoading, routeFetched } = useRouteData();
147
-
148
- // Set up store for routing when quote is selected
149
- useEffect(() => {
150
- if (selectedQuote && chainIdOut) {
151
- // Set the input token to the destination crypto
152
- setTokenIn(ETH_ADDRESS); // Native token
153
- setChainIdIn(chainIdOut);
154
- // Convert crypto amount to wei
155
- const decimals = 18; // Native tokens have 18 decimals
156
- const amountWei = denormalizeValue(
157
- selectedQuote.destinationAmount.toString(),
158
- decimals
159
- );
160
- setAmountIn(amountWei);
161
- }
162
- }, [selectedQuote, chainIdOut, setTokenIn, setChainIdIn, setAmountIn]);
163
-
164
- // Navigation
165
- const goBack = useCallback(() => {
166
- if (currentStep === CardBuyStep.ChooseAmount) {
167
- setFlow("");
168
- } else {
169
- setCurrentStep((prev) => prev - 1);
170
- }
171
- }, [currentStep, setFlow]);
172
-
173
- const goNext = useCallback(() => {
174
- setCurrentStep((prev) => prev + 1);
175
- }, []);
176
-
177
- // Sign userOp
178
- const handleSignUserOp = useCallback(async () => {
179
- if (!routeData?.userOp || !chainIdOut) return;
180
-
181
- try {
182
- const userOpHash = getUserOperationHash({
183
- chainId: chainIdOut,
184
- entryPointAddress: ENTRY_POINT_ADDRESS,
185
- entryPointVersion: "0.7",
186
- userOperation: routeData.userOp,
187
- });
188
-
189
- const signature = await signMessageAsync({
190
- message: { raw: userOpHash },
191
- });
192
-
193
- const signedOp = {
194
- ...routeData.userOp,
195
- signature,
196
- };
197
-
198
- setSignedUserOp(signedOp);
199
- goNext();
200
- } catch (err: any) {
201
- setError(err.message || "Failed to sign transaction");
202
- }
203
- }, [routeData, chainIdOut, signMessageAsync, goNext]);
204
-
205
- // Start MELD session
206
- const handleStartSession = useCallback(async () => {
207
- if (!selectedQuote || !smartAccountAddress || !destinationCryptoCode) return;
208
-
209
- try {
210
- await meldFlow.startSession({
211
- countryCode,
212
- sourceCurrencyCode: "USD",
213
- destinationCurrencyCode: destinationCryptoCode,
214
- sourceAmount: selectedQuote.sourceAmount,
215
- walletAddress: smartAccountAddress,
216
- serviceProvider: selectedQuote.serviceProvider,
217
- paymentMethodType: "CREDIT_DEBIT_CARD",
218
- externalSessionId: `enso-${Date.now()}`,
219
- });
220
- goNext();
221
- } catch (err: any) {
222
- setError(err.message || "Failed to create session");
223
- }
224
- }, [selectedQuote, smartAccountAddress, destinationCryptoCode, countryCode, meldFlow, goNext]);
225
-
226
- // Open widget
227
- const handleOpenWidget = useCallback(() => {
228
- meldFlow.openWidget();
229
- goNext();
230
- }, [meldFlow, goNext]);
231
-
232
- // Check if chain is supported
233
- if (!chainIdOut || !isMeldSupportedChain(chainIdOut)) {
234
- return (
235
- <>
236
- <Modal.Header>
237
- <HeaderWrapper>
238
- <IconButton onClick={() => setFlow("")} width="40px">
239
- <Icon as={ChevronLeft} color="fg.muted" width="16px" height="16px" />
240
- </IconButton>
241
- <Box>
242
- <HeaderTitle>Buy with Card</HeaderTitle>
243
- </Box>
244
- {handleClose && (
245
- <IconButton onClick={handleClose} width="40px">
246
- <Icon as={X} color="fg.muted" width="16px" height="16px" />
247
- </IconButton>
248
- )}
249
- </HeaderWrapper>
250
- </Modal.Header>
251
- <Modal.Body>
252
- <BodyWrapper>
253
- <Center py={8} flexDirection="column" gap={4}>
254
- <Icon as={XCircle} color="error" boxSize={12} />
255
- <Text color="fg.muted" textAlign="center">
256
- Card purchases are not supported for this chain.
257
- </Text>
258
- </Center>
259
- </BodyWrapper>
260
- </Modal.Body>
261
- </>
262
- );
263
- }
264
-
265
- // Render step content
266
- const renderStepContent = () => {
267
- switch (currentStep) {
268
- case CardBuyStep.ChooseAmount:
269
- return (
270
- <ChooseAmountStep
271
- fiatAmount={fiatAmount}
272
- setFiatAmount={setFiatAmount}
273
- quotes={quotes}
274
- quotesLoading={quotesLoading}
275
- selectedQuote={selectedQuote}
276
- setSelectedQuote={setSelectedQuote}
277
- destinationSymbol={destinationSymbol}
278
- onContinue={goNext}
279
- />
280
- );
281
-
282
- case CardBuyStep.SignUserOp:
283
- return (
284
- <SignUserOpStep
285
- selectedQuote={selectedQuote!}
286
- destinationSymbol={destinationSymbol}
287
- routeLoading={routeLoading}
288
- routeFetched={routeFetched}
289
- effectiveTokenOutData={effectiveTokenOutData}
290
- onSign={handleSignUserOp}
291
- error={error}
292
- />
293
- );
294
-
295
- case CardBuyStep.OpenWidget:
296
- return (
297
- <OpenWidgetStep
298
- selectedQuote={selectedQuote!}
299
- isCreatingSession={meldFlow.isCreatingSession}
300
- widgetUrl={meldFlow.widgetUrl}
301
- onCreateSession={handleStartSession}
302
- onOpenWidget={handleOpenWidget}
303
- error={error || meldFlow.sessionError?.message}
304
- />
305
- );
306
-
307
- case CardBuyStep.TrackPurchase:
308
- return (
309
- <TrackPurchaseStep
310
- transaction={meldFlow.transaction}
311
- isComplete={meldFlow.isComplete}
312
- isFailed={meldFlow.isFailed}
313
- onComplete={() => setCurrentStep(CardBuyStep.TrackUserOp)}
314
- />
315
- );
316
-
317
- case CardBuyStep.TrackUserOp:
318
- return (
319
- <TrackUserOpStep
320
- signedUserOp={signedUserOp}
321
- chainIdOut={chainIdOut!}
322
- onClose={handleClose}
323
- />
324
- );
325
-
326
- default:
327
- return null;
328
- }
329
- };
330
-
331
- const stepTitles: Record<CardBuyStep, string> = {
332
- [CardBuyStep.ChooseAmount]: "Buy with Card",
333
- [CardBuyStep.SignUserOp]: "Approve Transaction",
334
- [CardBuyStep.OpenWidget]: "Complete Purchase",
335
- [CardBuyStep.TrackPurchase]: "Processing Purchase",
336
- [CardBuyStep.TrackUserOp]: "Executing Transaction",
337
- };
338
-
339
- return (
340
- <>
341
- <Modal.Header>
342
- <HeaderWrapper>
343
- <IconButton onClick={goBack} width="40px">
344
- <Icon as={ChevronLeft} color="fg.muted" width="16px" height="16px" />
345
- </IconButton>
346
- <Box>
347
- <HeaderTitle>{stepTitles[currentStep]}</HeaderTitle>
348
- </Box>
349
- {handleClose && (
350
- <IconButton onClick={handleClose} width="40px">
351
- <Icon as={X} color="fg.muted" width="16px" height="16px" />
352
- </IconButton>
353
- )}
354
- </HeaderWrapper>
355
- </Modal.Header>
356
- <Modal.Body>{renderStepContent()}</Modal.Body>
357
- </>
358
- );
359
- };
360
-
361
- // Step Components
362
-
363
- interface ChooseAmountStepProps {
364
- fiatAmount: string;
365
- setFiatAmount: (amount: string) => void;
366
- quotes: MeldQuote[] | undefined;
367
- quotesLoading: boolean;
368
- selectedQuote: MeldQuote | null;
369
- setSelectedQuote: (quote: MeldQuote) => void;
370
- destinationSymbol: string;
371
- onContinue: () => void;
372
- }
373
-
374
- const ChooseAmountStep = ({
375
- fiatAmount,
376
- setFiatAmount,
377
- quotes,
378
- quotesLoading,
379
- selectedQuote,
380
- destinationSymbol,
381
- onContinue,
382
- }: ChooseAmountStepProps) => {
383
- const quickAmounts = [50, 100, 250, 500];
384
-
385
- return (
386
- <BodyWrapper>
387
- <Flex direction="column" gap={4}>
388
- {/* Amount Input */}
389
- <Box>
390
- <Text fontSize="sm" color="fg.muted" mb={2}>
391
- Amount (USD)
392
- </Text>
393
- <Input
394
- type="number"
395
- value={fiatAmount}
396
- onChange={(e) => setFiatAmount(e.target.value)}
397
- placeholder="Enter amount"
398
- min={10}
399
- />
400
- </Box>
401
-
402
- {/* Quick Amount Buttons */}
403
- <Flex gap={2} flexWrap="wrap">
404
- {quickAmounts.map((amount) => (
405
- <Button
406
- key={amount}
407
- variant={fiatAmount === String(amount) ? "solid" : "outline"}
408
- size="sm"
409
- onClick={() => setFiatAmount(String(amount))}
410
- >
411
- ${amount}
412
- </Button>
413
- ))}
414
- </Flex>
415
-
416
- {/* Quote Display */}
417
- <Box
418
- bg="bg.subtle"
419
- p={4}
420
- borderRadius="lg"
421
- border="1px solid"
422
- borderColor="border"
423
- >
424
- {quotesLoading ? (
425
- <Flex direction="column" gap={2}>
426
- <Skeleton height="20px" width="60%" />
427
- <Skeleton height="16px" width="40%" />
428
- </Flex>
429
- ) : selectedQuote ? (
430
- <Flex direction="column" gap={2}>
431
- <Flex justify="space-between" align="center">
432
- <Text fontSize="sm" color="fg.muted">
433
- You'll receive approximately:
434
- </Text>
435
- </Flex>
436
- <Text fontSize="xl" fontWeight="bold" color="fg">
437
- ~{selectedQuote.destinationAmount.toFixed(6)} {destinationSymbol}
438
- </Text>
439
- <Flex justify="space-between" fontSize="xs" color="fg.subtle">
440
- <Text>
441
- Rate: 1 {destinationSymbol} = ${(1 / selectedQuote.exchangeRate).toFixed(2)}
442
- </Text>
443
- <Text>
444
- Fee: ${selectedQuote.totalFee.toFixed(2)}
445
- </Text>
446
- </Flex>
447
- <Flex align="center" gap={1} fontSize="xs" color="fg.subtle">
448
- <Text>via</Text>
449
- <Text fontWeight="medium">
450
- {formatProviderName(selectedQuote.serviceProvider)}
451
- </Text>
452
- </Flex>
453
- </Flex>
454
- ) : (
455
- <Text fontSize="sm" color="fg.muted" textAlign="center">
456
- Enter an amount to see quote
457
- </Text>
458
- )}
459
- </Box>
460
-
461
- {/* Card Icons */}
462
- <Flex justify="center" gap={2} opacity={0.6}>
463
- {CARD_ICONS.map((icon, i) => (
464
- <Image key={i} src={icon} alt="Card" height="24px" />
465
- ))}
466
- </Flex>
467
-
468
- {/* Continue Button */}
469
- <Button
470
- onClick={onContinue}
471
- disabled={!selectedQuote || parseFloat(fiatAmount) < 10}
472
- width="100%"
473
- >
474
- Continue
475
- </Button>
476
-
477
- <Text fontSize="xs" color="fg.subtle" textAlign="center">
478
- You'll complete payment securely through our partner
479
- </Text>
480
- </Flex>
481
- </BodyWrapper>
482
- );
483
- };
484
-
485
- interface SignUserOpStepProps {
486
- selectedQuote: MeldQuote;
487
- destinationSymbol: string;
488
- routeLoading: boolean;
489
- routeFetched: boolean;
490
- effectiveTokenOutData: any;
491
- onSign: () => void;
492
- error: string | null;
493
- }
494
-
495
- const SignUserOpStep = ({
496
- selectedQuote,
497
- destinationSymbol,
498
- routeLoading,
499
- routeFetched,
500
- effectiveTokenOutData,
501
- onSign,
502
- error,
503
- }: SignUserOpStepProps) => {
504
- return (
505
- <BodyWrapper>
506
- <Flex direction="column" gap={4}>
507
- <Text fontSize="sm" color="fg.muted" textAlign="center">
508
- Sign to authorize the swap that will execute after your purchase completes
509
- </Text>
510
-
511
- <Box
512
- bg="bg.subtle"
513
- p={4}
514
- borderRadius="lg"
515
- border="1px solid"
516
- borderColor="border"
517
- >
518
- <Flex direction="column" gap={3}>
519
- <TransactionDetailRow
520
- label="From"
521
- value={`~${selectedQuote.destinationAmount.toFixed(6)} ${destinationSymbol}`}
522
- />
523
- <TransactionDetailRow
524
- label="To"
525
- value={
526
- effectiveTokenOutData
527
- ? `${effectiveTokenOutData.symbol}`
528
- : "Loading..."
529
- }
530
- />
531
- </Flex>
532
- </Box>
533
-
534
- {error && (
535
- <Text color="error" fontSize="sm" textAlign="center">
536
- {error}
537
- </Text>
538
- )}
539
-
540
- <Button
541
- onClick={onSign}
542
- disabled={routeLoading || !routeFetched}
543
- width="100%"
544
- >
545
- {routeLoading ? (
546
- <Flex align="center" gap={2}>
547
- <Spinner size="sm" />
548
- <Text>Preparing transaction...</Text>
549
- </Flex>
550
- ) : (
551
- "Sign in Wallet"
552
- )}
553
- </Button>
554
- </Flex>
555
- </BodyWrapper>
556
- );
557
- };
558
-
559
- interface OpenWidgetStepProps {
560
- selectedQuote: MeldQuote;
561
- isCreatingSession: boolean;
562
- widgetUrl: string | null;
563
- onCreateSession: () => void;
564
- onOpenWidget: () => void;
565
- error?: string | null;
566
- }
567
-
568
- const OpenWidgetStep = ({
569
- selectedQuote,
570
- isCreatingSession,
571
- widgetUrl,
572
- onCreateSession,
573
- onOpenWidget,
574
- error,
575
- }: OpenWidgetStepProps) => {
576
- // Auto-create session on mount
577
- useEffect(() => {
578
- if (!widgetUrl && !isCreatingSession) {
579
- onCreateSession();
580
- }
581
- }, [widgetUrl, isCreatingSession, onCreateSession]);
582
-
583
- return (
584
- <BodyWrapper>
585
- <Flex direction="column" gap={4} align="center">
586
- {isCreatingSession ? (
587
- <>
588
- <Spinner size="lg" color="primary" />
589
- <Text color="fg.muted">Preparing checkout...</Text>
590
- </>
591
- ) : widgetUrl ? (
592
- <>
593
- <Icon as={CreditCard} boxSize={12} color="primary" />
594
- <Text textAlign="center" color="fg">
595
- Complete your purchase of{" "}
596
- <Text as="span" fontWeight="bold">
597
- ${selectedQuote.sourceAmount}
598
- </Text>{" "}
599
- via {formatProviderName(selectedQuote.serviceProvider)}
600
- </Text>
601
-
602
- <Button onClick={onOpenWidget} width="100%">
603
- <Flex align="center" gap={2}>
604
- <Text>Open Payment</Text>
605
- <Icon as={ExternalLink} boxSize={4} />
606
- </Flex>
607
- </Button>
608
-
609
- <Text fontSize="xs" color="fg.subtle" textAlign="center">
610
- A new window will open for secure payment
611
- </Text>
612
- </>
613
- ) : error ? (
614
- <>
615
- <Icon as={XCircle} boxSize={12} color="error" />
616
- <Text color="error" textAlign="center">
617
- {error}
618
- </Text>
619
- <Button onClick={onCreateSession} variant="outline">
620
- Try Again
621
- </Button>
622
- </>
623
- ) : null}
624
- </Flex>
625
- </BodyWrapper>
626
- );
627
- };
628
-
629
- interface TrackPurchaseStepProps {
630
- transaction: any;
631
- isComplete: boolean;
632
- isFailed: boolean;
633
- onComplete: () => void;
634
- }
635
-
636
- const TrackPurchaseStep = ({
637
- transaction,
638
- isComplete,
639
- isFailed,
640
- onComplete,
641
- }: TrackPurchaseStepProps) => {
642
- // Auto-advance when complete
643
- useEffect(() => {
644
- if (isComplete) {
645
- const timer = setTimeout(onComplete, 1500);
646
- return () => clearTimeout(timer);
647
- }
648
- }, [isComplete, onComplete]);
649
-
650
- const statusMessages: Record<string, string> = {
651
- PENDING: "Waiting for payment...",
652
- AWAITING_PAYMENT: "Waiting for payment...",
653
- PAYMENT_RECEIVED: "Payment received, processing...",
654
- PROCESSING: "Processing your purchase...",
655
- COMPLETED: "Purchase complete!",
656
- FAILED: "Purchase failed",
657
- REFUNDED: "Purchase refunded",
658
- CANCELLED: "Purchase cancelled",
659
- };
660
-
661
- return (
662
- <BodyWrapper>
663
- <Flex direction="column" gap={4} align="center" py={4}>
664
- {isComplete ? (
665
- <Icon as={CheckCircle} boxSize={16} color="success" />
666
- ) : isFailed ? (
667
- <Icon as={XCircle} boxSize={16} color="error" />
668
- ) : (
669
- <CircleTimer duration={300} />
670
- )}
671
-
672
- <Text fontSize="lg" fontWeight="medium" color="fg">
673
- {transaction
674
- ? statusMessages[transaction.status] || transaction.status
675
- : "Waiting for transaction..."}
676
- </Text>
677
-
678
- {transaction && (
679
- <Box
680
- bg="bg.subtle"
681
- p={4}
682
- borderRadius="lg"
683
- width="100%"
684
- border="1px solid"
685
- borderColor="border"
686
- >
687
- <Flex direction="column" gap={2} fontSize="sm">
688
- <Flex justify="space-between">
689
- <Text color="fg.muted">Amount</Text>
690
- <Text color="fg">
691
- ${transaction.sourceAmount} → {transaction.destinationAmount}{" "}
692
- {transaction.destinationCurrencyCode?.split("_")[0]}
693
- </Text>
694
- </Flex>
695
- <Flex justify="space-between">
696
- <Text color="fg.muted">Provider</Text>
697
- <Text color="fg">
698
- {formatProviderName(transaction.serviceProvider)}
699
- </Text>
700
- </Flex>
701
- {transaction.transactionHash && (
702
- <Flex justify="space-between">
703
- <Text color="fg.muted">Tx Hash</Text>
704
- <Text color="fg" fontFamily="mono" fontSize="xs">
705
- {transaction.transactionHash.slice(0, 10)}...
706
- </Text>
707
- </Flex>
708
- )}
709
- </Flex>
710
- </Box>
711
- )}
712
-
713
- {!isComplete && !isFailed && (
714
- <Text fontSize="xs" color="fg.subtle" textAlign="center">
715
- This usually takes 1-5 minutes. You can close this window.
716
- </Text>
717
- )}
718
- </Flex>
719
- </BodyWrapper>
720
- );
721
- };
722
-
723
- interface TrackUserOpStepProps {
724
- signedUserOp: any;
725
- chainIdOut: number;
726
- onClose?: () => void;
727
- }
728
-
729
- const TrackUserOpStep = ({
730
- signedUserOp,
731
- chainIdOut,
732
- onClose,
733
- }: TrackUserOpStepProps) => {
734
- const [status, setStatus] = useState<"pending" | "executing" | "success" | "failed">("pending");
735
-
736
- // TODO: Implement actual userOp submission and tracking
737
- // This would be similar to the ExchangeFlow TrackUserOpStep
738
-
739
- return (
740
- <BodyWrapper>
741
- <Flex direction="column" gap={4} align="center" py={4}>
742
- {status === "success" ? (
743
- <>
744
- <Icon as={CheckCircle} boxSize={16} color="success" />
745
- <Text fontSize="lg" fontWeight="medium" color="fg">
746
- Transaction Complete!
747
- </Text>
748
- </>
749
- ) : status === "failed" ? (
750
- <>
751
- <Icon as={XCircle} boxSize={16} color="error" />
752
- <Text fontSize="lg" fontWeight="medium" color="fg">
753
- Transaction Failed
754
- </Text>
755
- </>
756
- ) : (
757
- <>
758
- <Spinner size="xl" color="primary" />
759
- <Text fontSize="lg" fontWeight="medium" color="fg">
760
- Executing your swap...
761
- </Text>
762
- <Text fontSize="sm" color="fg.muted" textAlign="center">
763
- Your funds have arrived. Now executing the final swap.
764
- </Text>
765
- </>
766
- )}
767
-
768
- {(status === "success" || status === "failed") && onClose && (
769
- <Button onClick={onClose} width="100%" mt={4}>
770
- Close
771
- </Button>
772
- )}
773
- </Flex>
774
- </BodyWrapper>
775
- );
776
- };
777
-
778
- export default CardBuyFlow;