@djangocfg/ext-payments 1.0.17 → 1.0.19

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 (46) hide show
  1. package/dist/config.cjs +1 -1
  2. package/dist/config.js +1 -1
  3. package/dist/index.cjs +1175 -290
  4. package/dist/index.d.cts +226 -80
  5. package/dist/index.d.ts +226 -80
  6. package/dist/index.js +1157 -255
  7. package/package.json +9 -9
  8. package/src/WalletPage.tsx +100 -0
  9. package/src/api/generated/ext_payments/CLAUDE.md +6 -4
  10. package/src/api/generated/ext_payments/_utils/fetchers/ext_payments__payments.ts +37 -6
  11. package/src/api/generated/ext_payments/_utils/hooks/ext_payments__payments.ts +34 -3
  12. package/src/api/generated/ext_payments/_utils/schemas/Balance.schema.ts +1 -1
  13. package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateResponse.schema.ts +22 -0
  14. package/src/api/generated/ext_payments/_utils/schemas/PaymentDetail.schema.ts +3 -3
  15. package/src/api/generated/ext_payments/_utils/schemas/PaymentList.schema.ts +2 -2
  16. package/src/api/generated/ext_payments/_utils/schemas/Transaction.schema.ts +1 -1
  17. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCancelResponse.schema.ts +22 -0
  18. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCreateResponse.schema.ts +22 -0
  19. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalDetail.schema.ts +5 -5
  20. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalList.schema.ts +2 -2
  21. package/src/api/generated/ext_payments/_utils/schemas/index.ts +3 -0
  22. package/src/api/generated/ext_payments/client.ts +1 -1
  23. package/src/api/generated/ext_payments/ext_payments__payments/client.ts +49 -4
  24. package/src/api/generated/ext_payments/ext_payments__payments/models.ts +33 -14
  25. package/src/api/generated/ext_payments/index.ts +1 -1
  26. package/src/api/generated/ext_payments/schema.json +167 -33
  27. package/src/components/AddFundsSheet.tsx +157 -73
  28. package/src/components/CurrencyCombobox.tsx +49 -0
  29. package/src/components/PaymentSheet.tsx +94 -32
  30. package/src/components/WithdrawSheet.tsx +121 -95
  31. package/src/components/WithdrawalSheet.tsx +332 -0
  32. package/src/components/index.ts +1 -8
  33. package/src/config.ts +1 -0
  34. package/src/contexts/WalletContext.tsx +10 -9
  35. package/src/contexts/index.ts +5 -1
  36. package/src/contexts/types.ts +46 -0
  37. package/src/hooks/index.ts +3 -0
  38. package/src/hooks/useCurrencyOptions.ts +79 -0
  39. package/src/hooks/useEstimate.ts +113 -0
  40. package/src/hooks/useWithdrawalEstimate.ts +117 -0
  41. package/src/index.ts +3 -0
  42. package/src/types/index.ts +78 -0
  43. package/src/utils/errors.ts +36 -0
  44. package/src/utils/format.ts +65 -0
  45. package/src/utils/index.ts +3 -0
  46. package/src/components/ResponsiveSheet.tsx +0 -151
@@ -2,12 +2,13 @@
2
2
  * Withdraw Sheet (Apple-style)
3
3
  *
4
4
  * Responsive: Dialog on desktop, Drawer on mobile
5
+ * Fetches real-time exchange rates and fees from API
5
6
  */
6
7
 
7
8
  'use client';
8
9
 
9
10
  import { useState, useMemo, useCallback } from 'react';
10
- import { RefreshCw, AlertCircle } from 'lucide-react';
11
+ import { RefreshCw, AlertCircle, Loader2 } from 'lucide-react';
11
12
  import { useForm } from 'react-hook-form';
12
13
  import { z } from 'zod';
13
14
  import { zodResolver } from '@hookform/resolvers/zod';
@@ -16,7 +17,6 @@ import {
16
17
  Alert,
17
18
  AlertDescription,
18
19
  Button,
19
- Combobox,
20
20
  Form,
21
21
  FormControl,
22
22
  FormField,
@@ -30,17 +30,21 @@ import {
30
30
  ResponsiveSheetDescription,
31
31
  ResponsiveSheetHeader,
32
32
  ResponsiveSheetTitle,
33
+ useLocalStorage,
33
34
  } from '@djangocfg/ui-core';
34
35
 
35
36
  import { useWallet } from '../contexts/WalletContext';
36
- import type { WithdrawalDetail } from '../api/generated/ext_payments/_utils/schemas';
37
+ import { useWithdrawalEstimate, useCurrencyOptions, useDefaultCurrency, useAutoSave } from '../hooks';
38
+ import { formatCryptoAmount, formatUsdAmount, extractErrorMessage } from '../utils';
39
+ import { CurrencyCombobox } from './CurrencyCombobox';
40
+ import type { WithdrawalDetail } from '../contexts/types';
37
41
 
38
42
  // ─────────────────────────────────────────────────────────────────────────────
39
43
  // Schema
40
44
  // ─────────────────────────────────────────────────────────────────────────────
41
45
 
42
46
  const WithdrawSchema = z.object({
43
- amount: z.number().min(10, 'Minimum $10.00'),
47
+ amount: z.number().min(10, 'Minimum $10'),
44
48
  currency: z.string().min(1, 'Select a currency'),
45
49
  wallet_address: z.string().min(26, 'Invalid wallet address'),
46
50
  });
@@ -57,12 +61,13 @@ interface WithdrawSheetProps {
57
61
  onSuccess?: (withdrawal: WithdrawalDetail) => void;
58
62
  }
59
63
 
60
- // ─────────────────────────────────────────────────────────────────────────────
61
- // Fee Config
62
- // ─────────────────────────────────────────────────────────────────────────────
64
+ // Storage
65
+ const STORAGE_KEY = 'payments:withdraw';
63
66
 
64
- const SERVICE_FEE_PERCENT = 0.01; // 1%
65
- const NETWORK_FEE_USD = 1.00; // $1
67
+ interface WithdrawSaved {
68
+ currency: string;
69
+ wallet: string;
70
+ }
66
71
 
67
72
  // ─────────────────────────────────────────────────────────────────────────────
68
73
  // Component
@@ -73,52 +78,61 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
73
78
  const [isSubmitting, setIsSubmitting] = useState(false);
74
79
  const [error, setError] = useState<string | null>(null);
75
80
 
81
+ // Remember last used currency and wallet address
82
+ const [saved, setSaved] = useLocalStorage<WithdrawSaved>(STORAGE_KEY, {
83
+ currency: '',
84
+ wallet: '',
85
+ });
86
+
76
87
  const form = useForm<WithdrawForm>({
77
88
  resolver: zodResolver(WithdrawSchema),
78
89
  defaultValues: {
79
90
  amount: 10,
80
- currency: '',
81
- wallet_address: '',
91
+ currency: saved.currency,
92
+ wallet_address: saved.wallet,
82
93
  },
83
94
  });
84
95
 
85
- // Currency options for combobox
86
- const currencyOptions = useMemo(() => {
87
- return currencies.map((c) => ({
88
- value: c.code,
89
- label: c.network ? `${c.code} (${c.network})` : c.code,
90
- rate: c.rate,
91
- network: c.network,
92
- }));
93
- }, [currencies]);
94
-
95
- // Set default currency when loaded
96
- useMemo(() => {
97
- if (currencyOptions.length > 0 && !form.getValues('currency')) {
98
- const usdt = currencyOptions.find(c => c.value.includes('USDT'));
99
- form.setValue('currency', usdt?.value || currencyOptions[0].value);
100
- }
101
- }, [currencyOptions, form]);
96
+ const watchedAmount = form.watch('amount');
97
+ const watchedCurrency = form.watch('currency');
98
+ const watchedWallet = form.watch('wallet_address');
99
+
100
+ // Auto-save to localStorage
101
+ useAutoSave(watchedCurrency, (v: string) => setSaved(prev => ({ ...prev, currency: v })));
102
+ useAutoSave(watchedWallet, (v: string) => setSaved(prev => ({ ...prev, wallet: v })), (v) => v.length >= 26);
102
103
 
103
- // Calculate fees and final amount
104
- const selectedCurrency = currencyOptions.find(c => c.value === form.watch('currency'));
105
- const amount = form.watch('amount') || 0;
104
+ // Currency options
105
+ const currencyOptions = useCurrencyOptions(currencies);
106
106
 
107
- const feeBreakdown = useMemo(() => {
108
- const serviceFee = amount * SERVICE_FEE_PERCENT;
109
- const networkFee = NETWORK_FEE_USD;
110
- const totalFee = serviceFee + networkFee;
111
- const finalAmount = Math.max(0, amount - totalFee);
112
- const cryptoAmount = selectedCurrency?.rate ? finalAmount / selectedCurrency.rate : null;
107
+ // Set default currency
108
+ useDefaultCurrency({
109
+ currencyOptions,
110
+ savedCurrency: saved.currency,
111
+ currentValue: watchedCurrency,
112
+ setValue: (v) => form.setValue('currency', v),
113
+ });
114
+
115
+ // Get selected currency for display
116
+ const selectedCurrency = currencyOptions.find(c => c.value === watchedCurrency);
117
+ const amount = watchedAmount || 0;
118
+
119
+ // Fetch withdrawal estimate with fees from API
120
+ const { estimate, isLoading: isLoadingEstimate } = useWithdrawalEstimate({
121
+ currencyCode: watchedCurrency,
122
+ amountUsd: amount,
123
+ minAmount: 10,
124
+ skip: amount < 10,
125
+ });
126
+
127
+ // Prepare crypto display data
128
+ const cryptoDisplay = useMemo(() => {
129
+ if (!selectedCurrency || !estimate) return null;
113
130
 
114
131
  return {
115
- serviceFee,
116
- networkFee,
117
- totalFee,
118
- finalAmount,
119
- cryptoAmount,
132
+ token: selectedCurrency.token,
133
+ cryptoAmount: formatCryptoAmount(estimate.estimatedAmount, estimate.isStablecoin),
120
134
  };
121
- }, [amount, selectedCurrency]);
135
+ }, [selectedCurrency, estimate]);
122
136
 
123
137
  // Check if user has enough balance
124
138
  const insufficientBalance = amount > balanceAmount;
@@ -138,26 +152,27 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
138
152
  form.reset();
139
153
  onOpenChange(false);
140
154
  onSuccess?.(result);
141
- } catch (err: any) {
142
- const message = err?.response?.data?.error
143
- || err?.response?.data?.message
144
- || err?.response?.data?.detail
145
- || err?.message
146
- || 'Failed to create withdrawal request';
147
- setError(message);
155
+ } catch (err) {
156
+ setError(extractErrorMessage(err, 'Failed to create withdrawal request'));
148
157
  } finally {
149
158
  setIsSubmitting(false);
150
159
  }
151
160
  }, [withdraw, form, onOpenChange, onSuccess]);
152
161
 
153
- // Reset on close
162
+ // Reset on open/close
154
163
  const handleOpenChange = useCallback((open: boolean) => {
155
- if (!open) {
164
+ if (open) {
165
+ // Reset form with saved values on open
166
+ form.reset({
167
+ amount: 10,
168
+ currency: saved.currency,
169
+ wallet_address: saved.wallet,
170
+ });
171
+ } else {
156
172
  setError(null);
157
- form.reset();
158
173
  }
159
174
  onOpenChange(open);
160
- }, [form, onOpenChange]);
175
+ }, [form, onOpenChange, saved]);
161
176
 
162
177
  return (
163
178
  <ResponsiveSheet open={open} onOpenChange={handleOpenChange}>
@@ -187,7 +202,7 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
187
202
  type="number"
188
203
  step="0.01"
189
204
  min="10"
190
- placeholder="10.00"
205
+ placeholder="10"
191
206
  className="pl-8 text-2xl h-14 font-semibold"
192
207
  {...field}
193
208
  onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
@@ -197,7 +212,7 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
197
212
  <FormMessage />
198
213
  {insufficientBalance && (
199
214
  <p className="text-sm text-destructive mt-1">
200
- Insufficient balance (Available: ${balanceAmount.toFixed(2)})
215
+ Insufficient balance (Available: ${formatUsdAmount(balanceAmount)})
201
216
  </p>
202
217
  )}
203
218
  </FormItem>
@@ -212,26 +227,11 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
212
227
  <FormItem>
213
228
  <FormLabel>Withdraw as</FormLabel>
214
229
  <FormControl>
215
- <Combobox
230
+ <CurrencyCombobox
216
231
  options={currencyOptions}
217
232
  value={field.value}
218
- onValueChange={field.onChange}
219
- placeholder="Select currency..."
220
- searchPlaceholder="Search..."
233
+ onChange={field.onChange}
221
234
  disabled={isLoadingCurrencies}
222
- className="h-14"
223
- renderOption={(option) => (
224
- <div className="flex items-center gap-3 flex-1">
225
- <TokenIcon symbol={option.value} size={24} />
226
- <span className="font-medium">{option.label}</span>
227
- </div>
228
- )}
229
- renderValue={(option) => option && (
230
- <div className="flex items-center gap-3">
231
- <TokenIcon symbol={option.value} size={24} />
232
- <span className="font-medium">{option.label}</span>
233
- </div>
234
- )}
235
235
  />
236
236
  </FormControl>
237
237
  <FormMessage />
@@ -263,30 +263,49 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
263
263
  <div className="bg-muted rounded-xl p-4 space-y-2">
264
264
  <div className="flex items-center justify-between text-sm">
265
265
  <span className="text-muted-foreground">Amount</span>
266
- <span>${amount.toFixed(2)}</span>
267
- </div>
268
- <div className="flex items-center justify-between text-sm">
269
- <span className="text-muted-foreground">Service fee (1%)</span>
270
- <span className="text-destructive">-${feeBreakdown.serviceFee.toFixed(2)}</span>
266
+ <span>${formatUsdAmount(amount)}</span>
271
267
  </div>
272
- <div className="flex items-center justify-between text-sm">
273
- <span className="text-muted-foreground">Network fee</span>
274
- <span className="text-destructive">-${feeBreakdown.networkFee.toFixed(2)}</span>
275
- </div>
276
- <div className="border-t pt-2 mt-2">
277
- <div className="flex items-center justify-between">
278
- <span className="font-medium">You will receive</span>
279
- <div className="text-right">
280
- <div className="font-semibold">${feeBreakdown.finalAmount.toFixed(2)}</div>
281
- {feeBreakdown.cryptoAmount !== null && (
282
- <div className="flex items-center gap-1 text-sm text-muted-foreground">
283
- <TokenIcon symbol={selectedCurrency.value} size={16} />
284
- <span className="font-mono">{feeBreakdown.cryptoAmount.toFixed(8)}</span>
268
+ {isLoadingEstimate ? (
269
+ <div className="flex items-center gap-2 text-sm text-muted-foreground py-2">
270
+ <Loader2 className="h-4 w-4 animate-spin" />
271
+ <span>Calculating fees...</span>
272
+ </div>
273
+ ) : estimate ? (
274
+ <>
275
+ <div className="flex items-center justify-between text-sm">
276
+ <span className="text-muted-foreground">
277
+ Service fee ({estimate.serviceFeePercent}%)
278
+ </span>
279
+ <span className="text-destructive">
280
+ -${formatUsdAmount(estimate.serviceFeeUsd)}
281
+ </span>
282
+ </div>
283
+ <div className="flex items-center justify-between text-sm">
284
+ <span className="text-muted-foreground">Network fee</span>
285
+ <span className="text-destructive">
286
+ -${formatUsdAmount(estimate.networkFeeUsd)}
287
+ </span>
288
+ </div>
289
+ <div className="border-t pt-2 mt-2">
290
+ <div className="flex items-center justify-between">
291
+ <span className="font-medium">You will receive</span>
292
+ <div className="text-right">
293
+ <div className="font-semibold">
294
+ ${formatUsdAmount(estimate.amountToReceive)}
295
+ </div>
296
+ {cryptoDisplay && (
297
+ <div className="flex items-center gap-1 text-sm text-muted-foreground">
298
+ <TokenIcon symbol={cryptoDisplay.token} size={16} />
299
+ <span className="font-mono">
300
+ {cryptoDisplay.cryptoAmount} {cryptoDisplay.token}
301
+ </span>
302
+ </div>
303
+ )}
285
304
  </div>
286
- )}
305
+ </div>
287
306
  </div>
288
- </div>
289
- </div>
307
+ </>
308
+ ) : null}
290
309
  </div>
291
310
  )}
292
311
 
@@ -310,7 +329,14 @@ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetPr
310
329
  type="submit"
311
330
  size="lg"
312
331
  className="w-full h-14 text-lg rounded-xl"
313
- disabled={isSubmitting || currencyOptions.length === 0 || insufficientBalance || feeBreakdown.finalAmount <= 0}
332
+ disabled={
333
+ isSubmitting ||
334
+ currencyOptions.length === 0 ||
335
+ insufficientBalance ||
336
+ !estimate ||
337
+ estimate.amountToReceive <= 0 ||
338
+ isLoadingEstimate
339
+ }
314
340
  >
315
341
  {isSubmitting ? (
316
342
  <>
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Withdrawal Details Sheet (Apple-style)
3
+ *
4
+ * Shows withdrawal request details and status
5
+ */
6
+
7
+ 'use client';
8
+
9
+ import { useMemo } from 'react';
10
+ import {
11
+ AlertCircle,
12
+ CheckCircle2,
13
+ Clock,
14
+ ExternalLink,
15
+ RefreshCw,
16
+ XCircle,
17
+ Ban,
18
+ } from 'lucide-react';
19
+ import moment from 'moment';
20
+ import useSWR from 'swr';
21
+
22
+ import {
23
+ Button,
24
+ CopyButton,
25
+ Skeleton,
26
+ TokenIcon,
27
+ ResponsiveSheet,
28
+ ResponsiveSheetContent,
29
+ ResponsiveSheetDescription,
30
+ ResponsiveSheetHeader,
31
+ ResponsiveSheetTitle,
32
+ } from '@djangocfg/ui-core';
33
+ import { cn } from '@djangocfg/ui-core/lib';
34
+
35
+ import { useWallet } from '../contexts/WalletContext';
36
+ import type { WithdrawalDetail } from '../api/generated/ext_payments/_utils/schemas';
37
+
38
+ // ─────────────────────────────────────────────────────────────────────────────
39
+ // Props
40
+ // ─────────────────────────────────────────────────────────────────────────────
41
+
42
+ interface WithdrawalSheetProps {
43
+ withdrawalId: string | null;
44
+ open: boolean;
45
+ onOpenChange: (open: boolean) => void;
46
+ }
47
+
48
+ // ─────────────────────────────────────────────────────────────────────────────
49
+ // Status Config
50
+ // ─────────────────────────────────────────────────────────────────────────────
51
+
52
+ const statusConfig: Record<string, { icon: any; color: string; bg: string; label: string; animate?: boolean }> = {
53
+ pending: {
54
+ icon: Clock,
55
+ color: 'text-yellow-500',
56
+ bg: 'bg-yellow-500/10',
57
+ label: 'Pending Approval',
58
+ },
59
+ approved: {
60
+ icon: CheckCircle2,
61
+ color: 'text-blue-500',
62
+ bg: 'bg-blue-500/10',
63
+ label: 'Approved',
64
+ },
65
+ processing: {
66
+ icon: RefreshCw,
67
+ color: 'text-blue-500',
68
+ bg: 'bg-blue-500/10',
69
+ label: 'Processing',
70
+ animate: true,
71
+ },
72
+ completed: {
73
+ icon: CheckCircle2,
74
+ color: 'text-green-500',
75
+ bg: 'bg-green-500/10',
76
+ label: 'Completed',
77
+ },
78
+ rejected: {
79
+ icon: XCircle,
80
+ color: 'text-red-500',
81
+ bg: 'bg-red-500/10',
82
+ label: 'Rejected',
83
+ },
84
+ cancelled: {
85
+ icon: Ban,
86
+ color: 'text-muted-foreground',
87
+ bg: 'bg-muted',
88
+ label: 'Cancelled',
89
+ },
90
+ };
91
+
92
+ // ─────────────────────────────────────────────────────────────────────────────
93
+ // Component
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+
96
+ export function WithdrawalSheet({ withdrawalId, open, onOpenChange }: WithdrawalSheetProps) {
97
+ const { getWithdrawalDetails, cancelWithdrawal, refreshWallet } = useWallet();
98
+
99
+ // Fetch withdrawal details when sheet is open
100
+ const { data: withdrawal, isLoading, error, mutate } = useSWR<WithdrawalDetail>(
101
+ open && withdrawalId ? ['withdrawal-details', withdrawalId] : null,
102
+ () => getWithdrawalDetails(withdrawalId!),
103
+ { refreshInterval: 30000 }
104
+ );
105
+
106
+ // Prepare all display data before render
107
+ const displayData = useMemo(() => {
108
+ const s = withdrawal?.status?.toLowerCase() || 'pending';
109
+ const config = statusConfig[s] || statusConfig.pending;
110
+
111
+ const isPending = s === 'pending';
112
+ const isCompleted = s === 'completed';
113
+ const isRejected = s === 'rejected';
114
+ const isCancelled = s === 'cancelled';
115
+ const isProcessing = s === 'processing' || s === 'approved';
116
+ const canCancel = isPending;
117
+
118
+ // Description text
119
+ let description = '';
120
+ if (isPending) description = 'Waiting for admin approval';
121
+ else if (isProcessing) description = 'Your withdrawal is being processed';
122
+ else if (isCompleted) description = 'Withdrawal completed successfully';
123
+ else if (isRejected) description = 'Withdrawal was rejected';
124
+ else if (isCancelled) description = 'Withdrawal was cancelled';
125
+
126
+ // Formatted values
127
+ const amountUsd = withdrawal?.amount_usd ? `$${parseFloat(withdrawal.amount_usd).toFixed(2)}` : '';
128
+ const finalAmountUsd = withdrawal?.final_amount_usd ? `$${parseFloat(withdrawal.final_amount_usd).toFixed(2)}` : '';
129
+ const totalFeeUsd = withdrawal?.total_fee_usd ? `$${parseFloat(withdrawal.total_fee_usd).toFixed(2)}` : '';
130
+ const createdAt = withdrawal?.created_at
131
+ ? moment.utc(withdrawal.created_at).local().format('MMM D, YYYY HH:mm')
132
+ : '';
133
+ const completedAt = withdrawal?.completed_at
134
+ ? moment.utc(withdrawal.completed_at).local().format('MMM D, YYYY HH:mm')
135
+ : null;
136
+
137
+ return {
138
+ status: s,
139
+ config,
140
+ isPending,
141
+ isCompleted,
142
+ isRejected,
143
+ isCancelled,
144
+ isProcessing,
145
+ canCancel,
146
+ description,
147
+ amountUsd,
148
+ finalAmountUsd,
149
+ totalFeeUsd,
150
+ createdAt,
151
+ completedAt,
152
+ };
153
+ }, [withdrawal]);
154
+
155
+ const { config, canCancel, description, amountUsd, finalAmountUsd, totalFeeUsd, createdAt, completedAt } = displayData;
156
+ const StatusIcon = config.icon;
157
+
158
+ // Handle cancel
159
+ const handleCancel = async () => {
160
+ if (!withdrawalId) return;
161
+ try {
162
+ await cancelWithdrawal(withdrawalId);
163
+ await mutate();
164
+ await refreshWallet();
165
+ } catch (err) {
166
+ console.error('Failed to cancel withdrawal:', err);
167
+ }
168
+ };
169
+
170
+ return (
171
+ <ResponsiveSheet open={open} onOpenChange={onOpenChange}>
172
+ <ResponsiveSheetContent className="sm:max-w-lg">
173
+ <ResponsiveSheetHeader>
174
+ <ResponsiveSheetTitle>Withdrawal Details</ResponsiveSheetTitle>
175
+ <ResponsiveSheetDescription>{description}</ResponsiveSheetDescription>
176
+ </ResponsiveSheetHeader>
177
+
178
+ <div className="p-4 sm:p-0 sm:mt-4 overflow-y-auto max-h-[70vh]">
179
+ {isLoading && (
180
+ <div className="space-y-6">
181
+ <Skeleton className="h-16 w-full rounded-xl" />
182
+ <Skeleton className="h-24 w-full rounded-xl" />
183
+ <Skeleton className="h-20 w-full rounded-xl" />
184
+ </div>
185
+ )}
186
+
187
+ {error && (
188
+ <div className="flex flex-col items-center justify-center py-12">
189
+ <XCircle className="h-12 w-12 text-destructive mb-4" />
190
+ <p className="text-sm text-muted-foreground mb-4">Failed to load withdrawal</p>
191
+ <Button onClick={() => mutate()}>Try Again</Button>
192
+ </div>
193
+ )}
194
+
195
+ {withdrawal && !isLoading && (
196
+ <div className="space-y-6">
197
+ {/* Status Badge */}
198
+ <div className={cn('flex items-center gap-3 p-4 rounded-xl', config.bg)}>
199
+ <StatusIcon className={cn('h-6 w-6', config.color, config.animate && 'animate-spin')} />
200
+ <div className="flex-1">
201
+ <div className="font-semibold">{config.label}</div>
202
+ {withdrawal.admin_notes && (
203
+ <div className="text-sm text-muted-foreground">{withdrawal.admin_notes}</div>
204
+ )}
205
+ </div>
206
+ </div>
207
+
208
+ {/* Amount Breakdown */}
209
+ <div className="bg-muted rounded-xl p-4 space-y-3">
210
+ <div className="flex items-center justify-between">
211
+ <span className="text-muted-foreground">Amount</span>
212
+ <span className="font-semibold">{amountUsd}</span>
213
+ </div>
214
+ {totalFeeUsd && (
215
+ <div className="flex items-center justify-between text-sm">
216
+ <span className="text-muted-foreground">Total fees</span>
217
+ <span className="text-destructive">-{totalFeeUsd}</span>
218
+ </div>
219
+ )}
220
+ <div className="flex items-center justify-between pt-2 border-t">
221
+ <span className="text-muted-foreground">You receive</span>
222
+ <div className="flex items-center gap-2">
223
+ <TokenIcon symbol={withdrawal.currency_code} size={24} />
224
+ <span className="font-mono font-bold text-lg">
225
+ {finalAmountUsd}
226
+ </span>
227
+ </div>
228
+ </div>
229
+ {withdrawal.crypto_amount && (
230
+ <div className="flex items-center justify-between text-sm">
231
+ <span className="text-muted-foreground">Crypto amount</span>
232
+ <span className="font-mono">{withdrawal.crypto_amount} {withdrawal.currency_token}</span>
233
+ </div>
234
+ )}
235
+ </div>
236
+
237
+ {/* Wallet Address */}
238
+ {withdrawal.wallet_address && (
239
+ <div className="space-y-2">
240
+ <label className="text-sm font-medium">Destination Wallet</label>
241
+ <div className="flex items-center gap-2">
242
+ <div className="flex-1 p-3 bg-muted rounded-xl font-mono text-sm break-all">
243
+ {withdrawal.wallet_address}
244
+ </div>
245
+ <CopyButton value={withdrawal.wallet_address} variant="outline" className="shrink-0" />
246
+ </div>
247
+ </div>
248
+ )}
249
+
250
+ {/* Transaction Hash (if completed) */}
251
+ {withdrawal.transaction_hash && (
252
+ <div className="space-y-2">
253
+ <label className="text-sm font-medium">Transaction Hash</label>
254
+ <div className="flex items-center gap-2">
255
+ <div className="flex-1 p-3 bg-muted rounded-xl font-mono text-sm break-all">
256
+ {withdrawal.transaction_hash}
257
+ </div>
258
+ <CopyButton value={withdrawal.transaction_hash} variant="outline" className="shrink-0" />
259
+ </div>
260
+ </div>
261
+ )}
262
+
263
+ {/* Explorer Link */}
264
+ {withdrawal.explorer_link && (
265
+ <Button
266
+ variant="outline"
267
+ className="w-full"
268
+ onClick={() => window.open(withdrawal.explorer_link!, '_blank')}
269
+ >
270
+ <ExternalLink className="h-4 w-4 mr-2" />
271
+ View on Explorer
272
+ </Button>
273
+ )}
274
+
275
+ {/* Cancel Button */}
276
+ {canCancel && (
277
+ <Button
278
+ variant="destructive"
279
+ className="w-full"
280
+ onClick={handleCancel}
281
+ >
282
+ <Ban className="h-4 w-4 mr-2" />
283
+ Cancel Withdrawal
284
+ </Button>
285
+ )}
286
+
287
+ {/* Metadata */}
288
+ <div className="space-y-2 text-xs text-muted-foreground pt-4 border-t">
289
+ <div className="flex justify-between">
290
+ <span>Withdrawal ID</span>
291
+ <span className="font-mono">{withdrawal.id}</span>
292
+ </div>
293
+ {withdrawal.internal_withdrawal_id && (
294
+ <div className="flex justify-between">
295
+ <span>Reference #</span>
296
+ <span className="font-mono">{withdrawal.internal_withdrawal_id}</span>
297
+ </div>
298
+ )}
299
+ <div className="flex justify-between">
300
+ <span>Created</span>
301
+ <span>{createdAt}</span>
302
+ </div>
303
+ {completedAt && (
304
+ <div className="flex justify-between">
305
+ <span>Completed</span>
306
+ <span>{completedAt}</span>
307
+ </div>
308
+ )}
309
+ {withdrawal.currency_network && (
310
+ <div className="flex justify-between">
311
+ <span>Network</span>
312
+ <span>{withdrawal.currency_network}</span>
313
+ </div>
314
+ )}
315
+ </div>
316
+
317
+ {/* Refresh Button */}
318
+ <Button
319
+ variant="ghost"
320
+ className="w-full"
321
+ onClick={() => mutate()}
322
+ >
323
+ <RefreshCw className="h-4 w-4 mr-2" />
324
+ Refresh Status
325
+ </Button>
326
+ </div>
327
+ )}
328
+ </div>
329
+ </ResponsiveSheetContent>
330
+ </ResponsiveSheet>
331
+ );
332
+ }
@@ -7,12 +7,5 @@ export { ActivityList } from './ActivityList';
7
7
  export { ActivityItem } from './ActivityItem';
8
8
  export { AddFundsSheet } from './AddFundsSheet';
9
9
  export { WithdrawSheet } from './WithdrawSheet';
10
+ export { WithdrawalSheet } from './WithdrawalSheet';
10
11
  export { PaymentSheet } from './PaymentSheet';
11
- export {
12
- ResponsiveSheet,
13
- ResponsiveSheetContent,
14
- ResponsiveSheetHeader,
15
- ResponsiveSheetTitle,
16
- ResponsiveSheetDescription,
17
- ResponsiveSheetFooter,
18
- } from './ResponsiveSheet';