@one_deploy/sdk 1.0.0

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 (83) hide show
  1. package/.turbo/turbo-build.log +0 -0
  2. package/.turbo/turbo-type-check.log +0 -0
  3. package/dist/config/index.d.mts +74 -0
  4. package/dist/config/index.d.ts +74 -0
  5. package/dist/config/index.js +242 -0
  6. package/dist/config/index.js.map +1 -0
  7. package/dist/config/index.mjs +224 -0
  8. package/dist/config/index.mjs.map +1 -0
  9. package/dist/engine-5ndtBaCr.d.ts +1039 -0
  10. package/dist/engine-CrlhH0nw.d.mts +1039 -0
  11. package/dist/hooks/index.d.mts +56 -0
  12. package/dist/hooks/index.d.ts +56 -0
  13. package/dist/hooks/index.js +1360 -0
  14. package/dist/hooks/index.js.map +1 -0
  15. package/dist/hooks/index.mjs +1356 -0
  16. package/dist/hooks/index.mjs.map +1 -0
  17. package/dist/index.d.mts +356 -0
  18. package/dist/index.d.ts +356 -0
  19. package/dist/index.js +5068 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/index.mjs +4949 -0
  22. package/dist/index.mjs.map +1 -0
  23. package/dist/price-CgqXPnT3.d.ts +13 -0
  24. package/dist/price-ClbLHHjv.d.mts +13 -0
  25. package/dist/providers/index.d.mts +121 -0
  26. package/dist/providers/index.d.ts +121 -0
  27. package/dist/providers/index.js +1642 -0
  28. package/dist/providers/index.js.map +1 -0
  29. package/dist/providers/index.mjs +1600 -0
  30. package/dist/providers/index.mjs.map +1 -0
  31. package/dist/react-native.d.mts +120 -0
  32. package/dist/react-native.d.ts +120 -0
  33. package/dist/react-native.js +1792 -0
  34. package/dist/react-native.js.map +1 -0
  35. package/dist/react-native.mjs +1755 -0
  36. package/dist/react-native.mjs.map +1 -0
  37. package/dist/services/index.d.mts +85 -0
  38. package/dist/services/index.d.ts +85 -0
  39. package/dist/services/index.js +1466 -0
  40. package/dist/services/index.js.map +1 -0
  41. package/dist/services/index.mjs +1458 -0
  42. package/dist/services/index.mjs.map +1 -0
  43. package/dist/types/index.d.mts +759 -0
  44. package/dist/types/index.d.ts +759 -0
  45. package/dist/types/index.js +4 -0
  46. package/dist/types/index.js.map +1 -0
  47. package/dist/types/index.mjs +3 -0
  48. package/dist/types/index.mjs.map +1 -0
  49. package/dist/utils/index.d.mts +36 -0
  50. package/dist/utils/index.d.ts +36 -0
  51. package/dist/utils/index.js +164 -0
  52. package/dist/utils/index.js.map +1 -0
  53. package/dist/utils/index.mjs +142 -0
  54. package/dist/utils/index.mjs.map +1 -0
  55. package/package.json +101 -0
  56. package/src/components/OneConnectButton.tsx +143 -0
  57. package/src/components/OneNFTGallery.tsx +324 -0
  58. package/src/components/OneOfframpWidget.tsx +660 -0
  59. package/src/components/OneOnrampWidget.tsx +596 -0
  60. package/src/components/OnePayWidget.tsx +160 -0
  61. package/src/components/OneReceiveWidget.tsx +272 -0
  62. package/src/components/OneSendWidget.tsx +248 -0
  63. package/src/components/OneSwapWidget.tsx +715 -0
  64. package/src/components/OneTransactionButton.tsx +150 -0
  65. package/src/components/OneWalletBalance.tsx +354 -0
  66. package/src/components/index.ts +24 -0
  67. package/src/config/index.ts +299 -0
  68. package/src/hooks/index.ts +2 -0
  69. package/src/hooks/useTokenPrice.ts +162 -0
  70. package/src/hooks/useWalletBalance.ts +98 -0
  71. package/src/index.ts +193 -0
  72. package/src/providers/OneProvider.tsx +452 -0
  73. package/src/providers/ThirdwebProvider.tsx +203 -0
  74. package/src/providers/index.ts +26 -0
  75. package/src/react-native.ts +378 -0
  76. package/src/services/engine.ts +1854 -0
  77. package/src/services/index.ts +30 -0
  78. package/src/services/price.ts +164 -0
  79. package/src/services/supabase.ts +180 -0
  80. package/src/types/index.ts +887 -0
  81. package/src/utils/index.ts +200 -0
  82. package/tsconfig.json +22 -0
  83. package/tsup.config.ts +25 -0
@@ -0,0 +1,660 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useEffect, useCallback } from 'react';
4
+ import { useActiveAccount, useWalletBalance } from 'thirdweb/react';
5
+ import { base } from 'thirdweb/chains';
6
+ import { useThirdwebClient } from '../providers/ThirdwebProvider';
7
+ import { getEngineUrl } from '../config';
8
+
9
+ // ===== Types =====
10
+
11
+ export interface OneOfframpWidgetProps {
12
+ // Configuration
13
+ defaultFiat?: string;
14
+ defaultCrypto?: string;
15
+ defaultAmount?: string;
16
+ defaultNetwork?: string;
17
+
18
+ // Currency options
19
+ supportedFiats?: string[];
20
+ supportedCryptos?: string[];
21
+ supportedNetworks?: string[];
22
+
23
+ // User info (pre-fill)
24
+ email?: string;
25
+ country?: string;
26
+
27
+ // Appearance
28
+ theme?: 'light' | 'dark';
29
+ accentColor?: string;
30
+ className?: string;
31
+ style?: React.CSSProperties;
32
+
33
+ // Display mode
34
+ mode?: 'form' | 'embed' | 'popup';
35
+ embedHeight?: number;
36
+
37
+ // Callbacks
38
+ onSuccess?: (transaction: OfframpTransaction) => void;
39
+ onError?: (error: Error) => void;
40
+ onClose?: () => void;
41
+ onQuoteUpdate?: (quote: OfframpQuote) => void;
42
+ }
43
+
44
+ export interface OfframpQuote {
45
+ cryptoCurrency: string;
46
+ cryptoAmount: number;
47
+ fiatCurrency: string;
48
+ fiatAmount: number;
49
+ network: string;
50
+ rate: number;
51
+ fees: {
52
+ network: number;
53
+ provider: number;
54
+ total: number;
55
+ };
56
+ estimatedTime: string;
57
+ }
58
+
59
+ export interface OfframpTransaction {
60
+ id: string;
61
+ status: 'pending' | 'processing' | 'completed' | 'failed';
62
+ cryptoCurrency: string;
63
+ cryptoAmount: number;
64
+ fiatCurrency: string;
65
+ fiatAmount?: number;
66
+ walletAddress: string;
67
+ txHash?: string;
68
+ provider: string;
69
+ }
70
+
71
+ // ===== Default Values =====
72
+
73
+ const DEFAULT_FIATS = ['USD', 'EUR', 'GBP', 'CNY', 'JPY', 'KRW', 'CAD', 'AUD'];
74
+ const DEFAULT_CRYPTOS = ['USDT', 'USDC', 'ETH', 'BTC', 'MATIC', 'BNB'];
75
+ const DEFAULT_NETWORKS = ['ethereum', 'polygon', 'arbitrum', 'optimism', 'base', 'bsc', 'avalanche'];
76
+
77
+ const NETWORK_DISPLAY_NAMES: Record<string, string> = {
78
+ ethereum: 'Ethereum',
79
+ polygon: 'Polygon',
80
+ arbitrum: 'Arbitrum',
81
+ optimism: 'Optimism',
82
+ base: 'Base',
83
+ bsc: 'BNB Chain',
84
+ avalanche: 'Avalanche',
85
+ solana: 'Solana',
86
+ };
87
+
88
+ // Approximate rates for quote estimation
89
+ const CRYPTO_RATES: Record<string, number> = {
90
+ ETH: 3500,
91
+ BTC: 95000,
92
+ USDT: 1,
93
+ USDC: 1,
94
+ MATIC: 0.85,
95
+ BNB: 650,
96
+ AVAX: 42,
97
+ SOL: 200,
98
+ DAI: 1,
99
+ };
100
+
101
+ // ===== Styles =====
102
+
103
+ const containerStyle: React.CSSProperties = {
104
+ display: 'flex',
105
+ flexDirection: 'column',
106
+ gap: '16px',
107
+ padding: '24px',
108
+ borderRadius: '16px',
109
+ border: '1px solid',
110
+ maxWidth: '420px',
111
+ };
112
+
113
+ const headerStyle: React.CSSProperties = {
114
+ display: 'flex',
115
+ justifyContent: 'space-between',
116
+ alignItems: 'center',
117
+ marginBottom: '8px',
118
+ };
119
+
120
+ const selectStyle: React.CSSProperties = {
121
+ padding: '12px 16px',
122
+ borderRadius: '12px',
123
+ fontSize: '16px',
124
+ outline: 'none',
125
+ cursor: 'pointer',
126
+ appearance: 'none',
127
+ backgroundRepeat: 'no-repeat',
128
+ backgroundPosition: 'right 12px center',
129
+ paddingRight: '40px',
130
+ };
131
+
132
+ const inputStyle: React.CSSProperties = {
133
+ padding: '12px 16px',
134
+ borderRadius: '12px',
135
+ fontSize: '18px',
136
+ fontWeight: 500,
137
+ outline: 'none',
138
+ width: '100%',
139
+ boxSizing: 'border-box',
140
+ };
141
+
142
+ const buttonStyle: React.CSSProperties = {
143
+ width: '100%',
144
+ padding: '16px',
145
+ border: 'none',
146
+ borderRadius: '12px',
147
+ fontSize: '16px',
148
+ fontWeight: 600,
149
+ cursor: 'pointer',
150
+ transition: 'background-color 0.2s',
151
+ };
152
+
153
+ const rowStyle: React.CSSProperties = {
154
+ display: 'flex',
155
+ gap: '12px',
156
+ alignItems: 'center',
157
+ };
158
+
159
+ const quoteBoxStyle: React.CSSProperties = {
160
+ padding: '16px',
161
+ borderRadius: '12px',
162
+ fontSize: '14px',
163
+ };
164
+
165
+ const balanceStyle: React.CSSProperties = {
166
+ display: 'flex',
167
+ justifyContent: 'space-between',
168
+ fontSize: '12px',
169
+ marginTop: '4px',
170
+ };
171
+
172
+ // ===== Component =====
173
+
174
+ export function OneOfframpWidget({
175
+ defaultFiat = 'USD',
176
+ defaultCrypto = 'USDT',
177
+ defaultAmount = '',
178
+ defaultNetwork = 'base',
179
+ supportedFiats = DEFAULT_FIATS,
180
+ supportedCryptos = DEFAULT_CRYPTOS,
181
+ supportedNetworks = DEFAULT_NETWORKS,
182
+ email,
183
+ country,
184
+ theme = 'dark',
185
+ accentColor = '#ef4444', // Red for sell
186
+ className,
187
+ style,
188
+ mode = 'form',
189
+ embedHeight = 600,
190
+ onSuccess,
191
+ onError,
192
+ onClose,
193
+ onQuoteUpdate,
194
+ }: OneOfframpWidgetProps) {
195
+ const client = useThirdwebClient();
196
+ const account = useActiveAccount();
197
+ const walletAddress = account?.address;
198
+
199
+ // Get balance for selected crypto
200
+ const { data: balanceData } = useWalletBalance({
201
+ client,
202
+ chain: base,
203
+ address: walletAddress,
204
+ });
205
+
206
+ // Form state
207
+ const [cryptoCurrency, setCryptoCurrency] = useState(defaultCrypto);
208
+ const [cryptoAmount, setCryptoAmount] = useState(defaultAmount);
209
+ const [fiatCurrency, setFiatCurrency] = useState(defaultFiat);
210
+ const [network, setNetwork] = useState(defaultNetwork);
211
+ const [isLoading, setIsLoading] = useState(false);
212
+ const [error, setError] = useState<string | null>(null);
213
+ const [quote, setQuote] = useState<OfframpQuote | null>(null);
214
+ const [widgetUrl, setWidgetUrl] = useState<string | null>(null);
215
+
216
+ const isDark = theme === 'dark';
217
+
218
+ // Calculate estimated quote
219
+ const calculateQuote = useCallback((): OfframpQuote | null => {
220
+ const amount = parseFloat(cryptoAmount);
221
+ if (isNaN(amount) || amount <= 0) return null;
222
+
223
+ const cryptoRate = CRYPTO_RATES[cryptoCurrency.toUpperCase()] || 1;
224
+ const grossAmount = amount * cryptoRate;
225
+ const providerFee = grossAmount * 0.025; // ~2.5% provider fee for offramp
226
+ const networkFee = cryptoCurrency === 'ETH' ? 5 : 1;
227
+ const totalFee = providerFee + networkFee;
228
+ const netAmount = grossAmount - totalFee;
229
+
230
+ return {
231
+ cryptoCurrency,
232
+ cryptoAmount: amount,
233
+ fiatCurrency,
234
+ fiatAmount: parseFloat(netAmount.toFixed(2)),
235
+ network,
236
+ rate: cryptoRate,
237
+ fees: {
238
+ network: networkFee,
239
+ provider: providerFee,
240
+ total: totalFee,
241
+ },
242
+ estimatedTime: '1-3 business days',
243
+ };
244
+ }, [cryptoAmount, cryptoCurrency, fiatCurrency, network]);
245
+
246
+ // Update quote when inputs change
247
+ useEffect(() => {
248
+ const newQuote = calculateQuote();
249
+ setQuote(newQuote);
250
+ if (newQuote) {
251
+ onQuoteUpdate?.(newQuote);
252
+ }
253
+ }, [calculateQuote, onQuoteUpdate]);
254
+
255
+ // Build Onramper widget URL (sell mode)
256
+ const buildWidgetUrl = useCallback(() => {
257
+ if (!walletAddress) return null;
258
+
259
+ const baseUrl = 'https://buy.onramper.com';
260
+ const params = new URLSearchParams();
261
+
262
+ // API key from engine or env
263
+ const apiKey = process.env.NEXT_PUBLIC_ONRAMPER_API_KEY || '';
264
+ if (apiKey) {
265
+ params.append('apiKey', apiKey);
266
+ }
267
+
268
+ // Wallet addresses
269
+ params.append('wallets', `ETH:${walletAddress},MATIC:${walletAddress},BNB:${walletAddress}`);
270
+ params.append('networkWallets',
271
+ `ethereum:${walletAddress},polygon:${walletAddress},arbitrum:${walletAddress},` +
272
+ `optimism:${walletAddress},base:${walletAddress},bsc:${walletAddress}`
273
+ );
274
+
275
+ // SELL MODE
276
+ params.append('mode', 'sell');
277
+ params.append('defaultCrypto', cryptoCurrency);
278
+ params.append('defaultFiat', fiatCurrency);
279
+ if (cryptoAmount) {
280
+ params.append('sellDefaultAmount', cryptoAmount);
281
+ }
282
+
283
+ // Network filter
284
+ if (network) {
285
+ params.append('onlyCryptoNetworks', network);
286
+ }
287
+
288
+ // User info
289
+ if (email) params.append('email', email);
290
+ if (country) params.append('country', country);
291
+
292
+ // UI customization
293
+ params.append('color', accentColor.replace('#', ''));
294
+ params.append('darkMode', isDark ? 'true' : 'false');
295
+ params.append('hideTopBar', 'false');
296
+ params.append('isInAppBrowser', 'true');
297
+
298
+ // Tracking
299
+ params.append('partnerContext', `onesdk_sell_${Date.now()}`);
300
+
301
+ return `${baseUrl}?${params.toString()}`;
302
+ }, [walletAddress, cryptoCurrency, fiatCurrency, cryptoAmount, network, email, country, accentColor, isDark]);
303
+
304
+ // Handle sell button click
305
+ const handleSell = async () => {
306
+ if (!walletAddress) {
307
+ setError('Please connect your wallet first');
308
+ return;
309
+ }
310
+
311
+ const amount = parseFloat(cryptoAmount);
312
+ if (isNaN(amount) || amount <= 0) {
313
+ setError('Please enter a valid amount');
314
+ return;
315
+ }
316
+
317
+ // Check balance
318
+ if (balanceData && cryptoCurrency === 'ETH') {
319
+ const balance = parseFloat(balanceData.displayValue);
320
+ if (amount > balance) {
321
+ setError(`Insufficient balance. You have ${balance.toFixed(4)} ${cryptoCurrency}`);
322
+ return;
323
+ }
324
+ }
325
+
326
+ setIsLoading(true);
327
+ setError(null);
328
+
329
+ try {
330
+ // Try Engine API first
331
+ const engineUrl = getEngineUrl();
332
+ const response = await fetch(`${engineUrl}/v1/fiat/offramp`, {
333
+ method: 'POST',
334
+ headers: { 'Content-Type': 'application/json' },
335
+ body: JSON.stringify({
336
+ walletAddress,
337
+ cryptoCurrency,
338
+ cryptoAmount: amount,
339
+ fiatCurrency,
340
+ network,
341
+ email,
342
+ }),
343
+ });
344
+
345
+ if (response.ok) {
346
+ const data = await response.json();
347
+ if (data.success && data.data?.widgetUrl) {
348
+ setWidgetUrl(data.data.widgetUrl);
349
+ if (mode === 'popup') {
350
+ window.open(data.data.widgetUrl, '_blank', 'width=450,height=700');
351
+ }
352
+ return;
353
+ }
354
+ }
355
+
356
+ // Fallback to direct URL
357
+ const directUrl = buildWidgetUrl();
358
+ if (directUrl) {
359
+ setWidgetUrl(directUrl);
360
+ if (mode === 'popup') {
361
+ window.open(directUrl, '_blank', 'width=450,height=700');
362
+ }
363
+ } else {
364
+ throw new Error('Failed to generate widget URL');
365
+ }
366
+ } catch (err) {
367
+ const error = err instanceof Error ? err : new Error('Failed to start sale');
368
+ setError(error.message);
369
+ onError?.(error);
370
+ } finally {
371
+ setIsLoading(false);
372
+ }
373
+ };
374
+
375
+ // Use max balance
376
+ const handleUseMax = () => {
377
+ if (balanceData && cryptoCurrency === 'ETH') {
378
+ const balance = parseFloat(balanceData.displayValue);
379
+ // Leave some for gas
380
+ const maxAmount = Math.max(0, balance - 0.005);
381
+ setCryptoAmount(maxAmount.toFixed(6));
382
+ }
383
+ };
384
+
385
+ // Handle closing embed
386
+ const handleCloseEmbed = () => {
387
+ setWidgetUrl(null);
388
+ onClose?.();
389
+ };
390
+
391
+ // Theme colors
392
+ const bgColor = isDark ? '#1f2937' : '#ffffff';
393
+ const borderColor = isDark ? '#374151' : '#e5e7eb';
394
+ const textColor = isDark ? '#ffffff' : '#111827';
395
+ const mutedColor = isDark ? '#9ca3af' : '#6b7280';
396
+ const inputBg = isDark ? '#374151' : '#f3f4f6';
397
+ const quoteBg = isDark ? '#374151' : '#f3f4f6';
398
+
399
+ // Embed mode - show iframe
400
+ if (mode === 'embed' && widgetUrl) {
401
+ return (
402
+ <div className={className} style={{ ...style }}>
403
+ <div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: '8px' }}>
404
+ <button
405
+ onClick={handleCloseEmbed}
406
+ style={{
407
+ background: 'none',
408
+ border: 'none',
409
+ color: mutedColor,
410
+ cursor: 'pointer',
411
+ fontSize: '14px',
412
+ }}
413
+ >
414
+ Close
415
+ </button>
416
+ </div>
417
+ <iframe
418
+ src={widgetUrl}
419
+ width="100%"
420
+ height={embedHeight}
421
+ style={{ border: 'none', borderRadius: '16px' }}
422
+ allow="payment; clipboard-read; clipboard-write"
423
+ />
424
+ </div>
425
+ );
426
+ }
427
+
428
+ // Form mode
429
+ return (
430
+ <div
431
+ className={className}
432
+ style={{
433
+ ...containerStyle,
434
+ backgroundColor: bgColor,
435
+ borderColor,
436
+ ...style,
437
+ }}
438
+ >
439
+ {/* Header */}
440
+ <div style={headerStyle}>
441
+ <h3 style={{ color: textColor, margin: 0, fontSize: '18px', fontWeight: 600 }}>
442
+ Sell Crypto
443
+ </h3>
444
+ <span style={{ color: mutedColor, fontSize: '12px' }}>
445
+ Cash out to bank
446
+ </span>
447
+ </div>
448
+
449
+ {/* Crypto Input */}
450
+ <div>
451
+ <label style={{ color: mutedColor, fontSize: '14px', marginBottom: '8px', display: 'block' }}>
452
+ You Sell
453
+ </label>
454
+ <div style={rowStyle}>
455
+ <input
456
+ type="number"
457
+ value={cryptoAmount}
458
+ onChange={(e) => setCryptoAmount(e.target.value)}
459
+ placeholder="0.0"
460
+ min="0"
461
+ step="any"
462
+ style={{
463
+ ...inputStyle,
464
+ flex: 1,
465
+ backgroundColor: inputBg,
466
+ border: `1px solid ${borderColor}`,
467
+ color: textColor,
468
+ }}
469
+ />
470
+ <select
471
+ value={cryptoCurrency}
472
+ onChange={(e) => setCryptoCurrency(e.target.value)}
473
+ style={{
474
+ ...selectStyle,
475
+ backgroundColor: inputBg,
476
+ border: `1px solid ${borderColor}`,
477
+ color: textColor,
478
+ minWidth: '100px',
479
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='${isDark ? '%239ca3af' : '%236b7280'}'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
480
+ backgroundSize: '16px',
481
+ }}
482
+ >
483
+ {supportedCryptos.map((crypto) => (
484
+ <option key={crypto} value={crypto}>{crypto}</option>
485
+ ))}
486
+ </select>
487
+ </div>
488
+ {/* Balance row */}
489
+ {balanceData && cryptoCurrency === 'ETH' && (
490
+ <div style={balanceStyle}>
491
+ <span style={{ color: mutedColor }}>
492
+ Balance: {parseFloat(balanceData.displayValue).toFixed(4)} ETH
493
+ </span>
494
+ <button
495
+ onClick={handleUseMax}
496
+ style={{
497
+ background: 'none',
498
+ border: 'none',
499
+ color: accentColor,
500
+ cursor: 'pointer',
501
+ fontSize: '12px',
502
+ padding: 0,
503
+ }}
504
+ >
505
+ Use Max
506
+ </button>
507
+ </div>
508
+ )}
509
+ </div>
510
+
511
+ {/* Network Selection */}
512
+ <div>
513
+ <label style={{ color: mutedColor, fontSize: '14px', marginBottom: '8px', display: 'block' }}>
514
+ Network
515
+ </label>
516
+ <select
517
+ value={network}
518
+ onChange={(e) => setNetwork(e.target.value)}
519
+ style={{
520
+ ...selectStyle,
521
+ width: '100%',
522
+ backgroundColor: inputBg,
523
+ border: `1px solid ${borderColor}`,
524
+ color: textColor,
525
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='${isDark ? '%239ca3af' : '%236b7280'}'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
526
+ backgroundSize: '16px',
527
+ }}
528
+ >
529
+ {supportedNetworks.map((net) => (
530
+ <option key={net} value={net}>
531
+ {NETWORK_DISPLAY_NAMES[net] || net}
532
+ </option>
533
+ ))}
534
+ </select>
535
+ </div>
536
+
537
+ {/* Fiat Output */}
538
+ <div>
539
+ <label style={{ color: mutedColor, fontSize: '14px', marginBottom: '8px', display: 'block' }}>
540
+ You Receive
541
+ </label>
542
+ <div style={rowStyle}>
543
+ <div
544
+ style={{
545
+ ...inputStyle,
546
+ flex: 1,
547
+ backgroundColor: inputBg,
548
+ border: `1px solid ${borderColor}`,
549
+ color: textColor,
550
+ display: 'flex',
551
+ alignItems: 'center',
552
+ }}
553
+ >
554
+ <span style={{ color: quote ? textColor : mutedColor }}>
555
+ {quote ? `~$${quote.fiatAmount.toLocaleString()}` : '$0.00'}
556
+ </span>
557
+ </div>
558
+ <select
559
+ value={fiatCurrency}
560
+ onChange={(e) => setFiatCurrency(e.target.value)}
561
+ style={{
562
+ ...selectStyle,
563
+ backgroundColor: inputBg,
564
+ border: `1px solid ${borderColor}`,
565
+ color: textColor,
566
+ minWidth: '100px',
567
+ backgroundImage: `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='${isDark ? '%239ca3af' : '%236b7280'}'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M19 9l-7 7-7-7'%3E%3C/path%3E%3C/svg%3E")`,
568
+ backgroundSize: '16px',
569
+ }}
570
+ >
571
+ {supportedFiats.map((fiat) => (
572
+ <option key={fiat} value={fiat}>{fiat}</option>
573
+ ))}
574
+ </select>
575
+ </div>
576
+ </div>
577
+
578
+ {/* Quote Summary */}
579
+ {quote && (
580
+ <div style={{ ...quoteBoxStyle, backgroundColor: quoteBg }}>
581
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
582
+ <span style={{ color: mutedColor }}>Rate</span>
583
+ <span style={{ color: textColor }}>
584
+ 1 {cryptoCurrency} = ${quote.rate.toLocaleString()}
585
+ </span>
586
+ </div>
587
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: '8px' }}>
588
+ <span style={{ color: mutedColor }}>Fees</span>
589
+ <span style={{ color: textColor }}>
590
+ ~${quote.fees.total.toFixed(2)}
591
+ </span>
592
+ </div>
593
+ <div style={{ display: 'flex', justifyContent: 'space-between' }}>
594
+ <span style={{ color: mutedColor }}>Est. Arrival</span>
595
+ <span style={{ color: textColor }}>{quote.estimatedTime}</span>
596
+ </div>
597
+ </div>
598
+ )}
599
+
600
+ {/* Warning */}
601
+ <div style={{
602
+ padding: '12px',
603
+ backgroundColor: isDark ? 'rgba(251, 191, 36, 0.1)' : 'rgba(251, 191, 36, 0.15)',
604
+ borderRadius: '8px',
605
+ fontSize: '12px',
606
+ color: '#fbbf24',
607
+ }}>
608
+ Bank transfers typically take 1-3 business days. KYC verification may be required.
609
+ </div>
610
+
611
+ {/* Error Message */}
612
+ {error && (
613
+ <div style={{
614
+ color: '#ef4444',
615
+ fontSize: '14px',
616
+ padding: '12px',
617
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
618
+ borderRadius: '8px',
619
+ }}>
620
+ {error}
621
+ </div>
622
+ )}
623
+
624
+ {/* Sell Button */}
625
+ <button
626
+ onClick={handleSell}
627
+ disabled={isLoading || !walletAddress}
628
+ style={{
629
+ ...buttonStyle,
630
+ backgroundColor: isLoading || !walletAddress ? '#6b7280' : accentColor,
631
+ color: '#ffffff',
632
+ cursor: isLoading || !walletAddress ? 'not-allowed' : 'pointer',
633
+ }}
634
+ >
635
+ {isLoading ? 'Loading...' : !walletAddress ? 'Connect Wallet' : `Sell ${cryptoCurrency}`}
636
+ </button>
637
+
638
+ {/* Wallet Address Display */}
639
+ {walletAddress && (
640
+ <div style={{ textAlign: 'center', color: mutedColor, fontSize: '12px' }}>
641
+ Selling from: {walletAddress.slice(0, 6)}...{walletAddress.slice(-4)}
642
+ </div>
643
+ )}
644
+ </div>
645
+ );
646
+ }
647
+
648
+ // ===== Presets =====
649
+
650
+ export function OneSellUSDTWidget(props: Omit<OneOfframpWidgetProps, 'defaultCrypto'>) {
651
+ return <OneOfframpWidget {...props} defaultCrypto="USDT" />;
652
+ }
653
+
654
+ export function OneSellUSDCWidget(props: Omit<OneOfframpWidgetProps, 'defaultCrypto'>) {
655
+ return <OneOfframpWidget {...props} defaultCrypto="USDC" />;
656
+ }
657
+
658
+ export function OneSellETHWidget(props: Omit<OneOfframpWidgetProps, 'defaultCrypto'>) {
659
+ return <OneOfframpWidget {...props} defaultCrypto="ETH" />;
660
+ }