@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.
- package/dist/checkout-widget.es.js +22910 -16751
- package/dist/checkout-widget.es.js.map +1 -1
- package/dist/checkout-widget.umd.js +62 -54
- package/dist/checkout-widget.umd.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/enso-api.yaml +1 -1
- package/orval.config.ts +2 -2
- package/package.json +1 -1
- package/src/components/Checkout.tsx +22 -2
- package/src/components/steps/ExchangeFlow.tsx +38 -31
- package/src/components/steps/FlowSelector.tsx +119 -16
- package/src/components/steps/WalletFlow/WalletFlow.tsx +19 -14
- package/src/enso-api/custom-instance.ts +2 -2
- package/src/enso-api/index.ts +41 -41
- package/src/types/index.ts +4 -0
- package/src/util/tx-tracker.tsx +1 -1
- package/src/components/steps/CardBuyFlow.tsx +0 -778
- package/src/util/meld-hooks.tsx +0 -319
|
@@ -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;
|