@djangocfg/ext-payments 1.0.14 → 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 (76) hide show
  1. package/dist/config.cjs +5 -8
  2. package/dist/config.js +5 -8
  3. package/dist/index.cjs +1906 -1043
  4. package/dist/index.d.cts +644 -59
  5. package/dist/index.d.ts +644 -59
  6. package/dist/index.js +1886 -1040
  7. package/package.json +13 -16
  8. package/src/WalletPage.tsx +100 -0
  9. package/src/api/generated/ext_payments/CLAUDE.md +10 -4
  10. package/src/api/generated/ext_payments/_utils/fetchers/ext_payments__payments.ts +268 -5
  11. package/src/api/generated/ext_payments/_utils/hooks/ext_payments__payments.ts +102 -3
  12. package/src/api/generated/ext_payments/_utils/schemas/Balance.schema.ts +1 -1
  13. package/src/api/generated/ext_payments/_utils/schemas/PaginatedWithdrawalListList.schema.ts +24 -0
  14. package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateRequest.schema.ts +21 -0
  15. package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateResponse.schema.ts +22 -0
  16. package/src/api/generated/ext_payments/_utils/schemas/PaymentDetail.schema.ts +3 -3
  17. package/src/api/generated/ext_payments/_utils/schemas/PaymentList.schema.ts +2 -2
  18. package/src/api/generated/ext_payments/_utils/schemas/Transaction.schema.ts +1 -1
  19. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCancelResponse.schema.ts +22 -0
  20. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCreateRequest.schema.ts +21 -0
  21. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCreateResponse.schema.ts +22 -0
  22. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalDetail.schema.ts +42 -0
  23. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalList.schema.ts +29 -0
  24. package/src/api/generated/ext_payments/_utils/schemas/index.ts +8 -0
  25. package/src/api/generated/ext_payments/client.ts +1 -1
  26. package/src/api/generated/ext_payments/enums.ts +36 -0
  27. package/src/api/generated/ext_payments/ext_payments__payments/client.ts +104 -6
  28. package/src/api/generated/ext_payments/ext_payments__payments/models.ts +168 -8
  29. package/src/api/generated/ext_payments/index.ts +1 -1
  30. package/src/api/generated/ext_payments/schema.json +752 -42
  31. package/src/components/ActivityItem.tsx +118 -0
  32. package/src/components/ActivityList.tsx +93 -0
  33. package/src/components/AddFundsSheet.tsx +342 -0
  34. package/src/components/BalanceHero.tsx +102 -0
  35. package/src/components/CurrencyCombobox.tsx +49 -0
  36. package/src/components/PaymentSheet.tsx +352 -0
  37. package/src/components/WithdrawSheet.tsx +355 -0
  38. package/src/components/WithdrawalSheet.tsx +332 -0
  39. package/src/components/index.ts +11 -0
  40. package/src/config.ts +1 -0
  41. package/src/contexts/WalletContext.tsx +356 -0
  42. package/src/contexts/index.ts +13 -42
  43. package/src/contexts/types.ts +43 -37
  44. package/src/hooks/index.ts +3 -20
  45. package/src/hooks/useCurrencyOptions.ts +79 -0
  46. package/src/hooks/useEstimate.ts +113 -0
  47. package/src/hooks/useWithdrawalEstimate.ts +117 -0
  48. package/src/index.ts +9 -18
  49. package/src/types/index.ts +78 -0
  50. package/src/utils/errors.ts +36 -0
  51. package/src/utils/format.ts +65 -0
  52. package/src/utils/index.ts +3 -0
  53. package/src/contexts/BalancesContext.tsx +0 -63
  54. package/src/contexts/CurrenciesContext.tsx +0 -64
  55. package/src/contexts/OverviewContext.tsx +0 -173
  56. package/src/contexts/PaymentsContext.tsx +0 -122
  57. package/src/contexts/PaymentsExtensionProvider.tsx +0 -56
  58. package/src/contexts/README.md +0 -201
  59. package/src/contexts/RootPaymentsContext.tsx +0 -66
  60. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -90
  61. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -274
  62. package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -287
  63. package/src/layouts/PaymentsLayout/components/index.ts +0 -2
  64. package/src/layouts/PaymentsLayout/events.ts +0 -47
  65. package/src/layouts/PaymentsLayout/index.ts +0 -16
  66. package/src/layouts/PaymentsLayout/types.ts +0 -6
  67. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -121
  68. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -139
  69. package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
  70. package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -21
  71. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -279
  72. package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
  73. package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -18
  74. package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -260
  75. package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
  76. package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -18
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Withdraw Sheet (Apple-style)
3
+ *
4
+ * Responsive: Dialog on desktop, Drawer on mobile
5
+ * Fetches real-time exchange rates and fees from API
6
+ */
7
+
8
+ 'use client';
9
+
10
+ import { useState, useMemo, useCallback } from 'react';
11
+ import { RefreshCw, AlertCircle, Loader2 } from 'lucide-react';
12
+ import { useForm } from 'react-hook-form';
13
+ import { z } from 'zod';
14
+ import { zodResolver } from '@hookform/resolvers/zod';
15
+
16
+ import {
17
+ Alert,
18
+ AlertDescription,
19
+ Button,
20
+ Form,
21
+ FormControl,
22
+ FormField,
23
+ FormItem,
24
+ FormLabel,
25
+ FormMessage,
26
+ Input,
27
+ TokenIcon,
28
+ ResponsiveSheet,
29
+ ResponsiveSheetContent,
30
+ ResponsiveSheetDescription,
31
+ ResponsiveSheetHeader,
32
+ ResponsiveSheetTitle,
33
+ useLocalStorage,
34
+ } from '@djangocfg/ui-core';
35
+
36
+ import { useWallet } from '../contexts/WalletContext';
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';
41
+
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ // Schema
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ const WithdrawSchema = z.object({
47
+ amount: z.number().min(10, 'Minimum $10'),
48
+ currency: z.string().min(1, 'Select a currency'),
49
+ wallet_address: z.string().min(26, 'Invalid wallet address'),
50
+ });
51
+
52
+ type WithdrawForm = z.infer<typeof WithdrawSchema>;
53
+
54
+ // ─────────────────────────────────────────────────────────────────────────────
55
+ // Props
56
+ // ─────────────────────────────────────────────────────────────────────────────
57
+
58
+ interface WithdrawSheetProps {
59
+ open: boolean;
60
+ onOpenChange: (open: boolean) => void;
61
+ onSuccess?: (withdrawal: WithdrawalDetail) => void;
62
+ }
63
+
64
+ // Storage
65
+ const STORAGE_KEY = 'payments:withdraw';
66
+
67
+ interface WithdrawSaved {
68
+ currency: string;
69
+ wallet: string;
70
+ }
71
+
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+ // Component
74
+ // ─────────────────────────────────────────────────────────────────────────────
75
+
76
+ export function WithdrawSheet({ open, onOpenChange, onSuccess }: WithdrawSheetProps) {
77
+ const { currencies, isLoadingCurrencies, withdraw, balanceAmount } = useWallet();
78
+ const [isSubmitting, setIsSubmitting] = useState(false);
79
+ const [error, setError] = useState<string | null>(null);
80
+
81
+ // Remember last used currency and wallet address
82
+ const [saved, setSaved] = useLocalStorage<WithdrawSaved>(STORAGE_KEY, {
83
+ currency: '',
84
+ wallet: '',
85
+ });
86
+
87
+ const form = useForm<WithdrawForm>({
88
+ resolver: zodResolver(WithdrawSchema),
89
+ defaultValues: {
90
+ amount: 10,
91
+ currency: saved.currency,
92
+ wallet_address: saved.wallet,
93
+ },
94
+ });
95
+
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);
103
+
104
+ // Currency options
105
+ const currencyOptions = useCurrencyOptions(currencies);
106
+
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;
130
+
131
+ return {
132
+ token: selectedCurrency.token,
133
+ cryptoAmount: formatCryptoAmount(estimate.estimatedAmount, estimate.isStablecoin),
134
+ };
135
+ }, [selectedCurrency, estimate]);
136
+
137
+ // Check if user has enough balance
138
+ const insufficientBalance = amount > balanceAmount;
139
+
140
+ // Handle submit
141
+ const handleSubmit = useCallback(async (data: WithdrawForm) => {
142
+ try {
143
+ setIsSubmitting(true);
144
+ setError(null);
145
+
146
+ const result = await withdraw({
147
+ amount_usd: String(data.amount),
148
+ currency_code: data.currency,
149
+ wallet_address: data.wallet_address,
150
+ });
151
+
152
+ form.reset();
153
+ onOpenChange(false);
154
+ onSuccess?.(result);
155
+ } catch (err) {
156
+ setError(extractErrorMessage(err, 'Failed to create withdrawal request'));
157
+ } finally {
158
+ setIsSubmitting(false);
159
+ }
160
+ }, [withdraw, form, onOpenChange, onSuccess]);
161
+
162
+ // Reset on open/close
163
+ const handleOpenChange = useCallback((open: boolean) => {
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 {
172
+ setError(null);
173
+ }
174
+ onOpenChange(open);
175
+ }, [form, onOpenChange, saved]);
176
+
177
+ return (
178
+ <ResponsiveSheet open={open} onOpenChange={handleOpenChange}>
179
+ <ResponsiveSheetContent className="sm:max-w-md">
180
+ <ResponsiveSheetHeader>
181
+ <ResponsiveSheetTitle>Withdraw</ResponsiveSheetTitle>
182
+ <ResponsiveSheetDescription>
183
+ Withdraw funds to your cryptocurrency wallet
184
+ </ResponsiveSheetDescription>
185
+ </ResponsiveSheetHeader>
186
+
187
+ <Form {...form}>
188
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 p-4 sm:p-0 sm:mt-4">
189
+ {/* Amount Input */}
190
+ <FormField
191
+ control={form.control}
192
+ name="amount"
193
+ render={({ field }) => (
194
+ <FormItem>
195
+ <FormLabel>Amount (USD)</FormLabel>
196
+ <FormControl>
197
+ <div className="relative">
198
+ <span className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground text-lg">
199
+ $
200
+ </span>
201
+ <Input
202
+ type="number"
203
+ step="0.01"
204
+ min="10"
205
+ placeholder="10"
206
+ className="pl-8 text-2xl h-14 font-semibold"
207
+ {...field}
208
+ onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
209
+ />
210
+ </div>
211
+ </FormControl>
212
+ <FormMessage />
213
+ {insufficientBalance && (
214
+ <p className="text-sm text-destructive mt-1">
215
+ Insufficient balance (Available: ${formatUsdAmount(balanceAmount)})
216
+ </p>
217
+ )}
218
+ </FormItem>
219
+ )}
220
+ />
221
+
222
+ {/* Currency Selection */}
223
+ <FormField
224
+ control={form.control}
225
+ name="currency"
226
+ render={({ field }) => (
227
+ <FormItem>
228
+ <FormLabel>Withdraw as</FormLabel>
229
+ <FormControl>
230
+ <CurrencyCombobox
231
+ options={currencyOptions}
232
+ value={field.value}
233
+ onChange={field.onChange}
234
+ disabled={isLoadingCurrencies}
235
+ />
236
+ </FormControl>
237
+ <FormMessage />
238
+ </FormItem>
239
+ )}
240
+ />
241
+
242
+ {/* Wallet Address */}
243
+ <FormField
244
+ control={form.control}
245
+ name="wallet_address"
246
+ render={({ field }) => (
247
+ <FormItem>
248
+ <FormLabel>Wallet Address</FormLabel>
249
+ <FormControl>
250
+ <Input
251
+ placeholder="Enter your wallet address"
252
+ className="font-mono text-sm"
253
+ {...field}
254
+ />
255
+ </FormControl>
256
+ <FormMessage />
257
+ </FormItem>
258
+ )}
259
+ />
260
+
261
+ {/* Fee Breakdown */}
262
+ {amount >= 10 && selectedCurrency && (
263
+ <div className="bg-muted rounded-xl p-4 space-y-2">
264
+ <div className="flex items-center justify-between text-sm">
265
+ <span className="text-muted-foreground">Amount</span>
266
+ <span>${formatUsdAmount(amount)}</span>
267
+ </div>
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
+ )}
304
+ </div>
305
+ </div>
306
+ </div>
307
+ </>
308
+ ) : null}
309
+ </div>
310
+ )}
311
+
312
+ {/* Warning */}
313
+ <Alert>
314
+ <AlertCircle className="h-4 w-4" />
315
+ <AlertDescription>
316
+ Withdrawal requests require admin approval. Processing may take 24-48 hours.
317
+ </AlertDescription>
318
+ </Alert>
319
+
320
+ {/* Error */}
321
+ {error && (
322
+ <Alert variant="destructive">
323
+ <AlertDescription>{error}</AlertDescription>
324
+ </Alert>
325
+ )}
326
+
327
+ {/* Submit Button */}
328
+ <Button
329
+ type="submit"
330
+ size="lg"
331
+ className="w-full h-14 text-lg rounded-xl"
332
+ disabled={
333
+ isSubmitting ||
334
+ currencyOptions.length === 0 ||
335
+ insufficientBalance ||
336
+ !estimate ||
337
+ estimate.amountToReceive <= 0 ||
338
+ isLoadingEstimate
339
+ }
340
+ >
341
+ {isSubmitting ? (
342
+ <>
343
+ <RefreshCw className="h-5 w-5 mr-2 animate-spin" />
344
+ Submitting...
345
+ </>
346
+ ) : (
347
+ 'Request Withdrawal'
348
+ )}
349
+ </Button>
350
+ </form>
351
+ </Form>
352
+ </ResponsiveSheetContent>
353
+ </ResponsiveSheet>
354
+ );
355
+ }
@@ -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
+ }