@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.
- package/dist/config.cjs +1 -1
- package/dist/config.js +1 -1
- package/dist/index.cjs +1175 -290
- package/dist/index.d.cts +226 -80
- package/dist/index.d.ts +226 -80
- package/dist/index.js +1157 -255
- package/package.json +9 -9
- package/src/WalletPage.tsx +100 -0
- package/src/api/generated/ext_payments/CLAUDE.md +6 -4
- package/src/api/generated/ext_payments/_utils/fetchers/ext_payments__payments.ts +37 -6
- package/src/api/generated/ext_payments/_utils/hooks/ext_payments__payments.ts +34 -3
- package/src/api/generated/ext_payments/_utils/schemas/Balance.schema.ts +1 -1
- package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateResponse.schema.ts +22 -0
- package/src/api/generated/ext_payments/_utils/schemas/PaymentDetail.schema.ts +3 -3
- package/src/api/generated/ext_payments/_utils/schemas/PaymentList.schema.ts +2 -2
- package/src/api/generated/ext_payments/_utils/schemas/Transaction.schema.ts +1 -1
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCancelResponse.schema.ts +22 -0
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCreateResponse.schema.ts +22 -0
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalDetail.schema.ts +5 -5
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalList.schema.ts +2 -2
- package/src/api/generated/ext_payments/_utils/schemas/index.ts +3 -0
- package/src/api/generated/ext_payments/client.ts +1 -1
- package/src/api/generated/ext_payments/ext_payments__payments/client.ts +49 -4
- package/src/api/generated/ext_payments/ext_payments__payments/models.ts +33 -14
- package/src/api/generated/ext_payments/index.ts +1 -1
- package/src/api/generated/ext_payments/schema.json +167 -33
- package/src/components/AddFundsSheet.tsx +157 -73
- package/src/components/CurrencyCombobox.tsx +49 -0
- package/src/components/PaymentSheet.tsx +94 -32
- package/src/components/WithdrawSheet.tsx +121 -95
- package/src/components/WithdrawalSheet.tsx +332 -0
- package/src/components/index.ts +1 -8
- package/src/config.ts +1 -0
- package/src/contexts/WalletContext.tsx +10 -9
- package/src/contexts/index.ts +5 -1
- package/src/contexts/types.ts +46 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useCurrencyOptions.ts +79 -0
- package/src/hooks/useEstimate.ts +113 -0
- package/src/hooks/useWithdrawalEstimate.ts +117 -0
- package/src/index.ts +3 -0
- package/src/types/index.ts +78 -0
- package/src/utils/errors.ts +36 -0
- package/src/utils/format.ts +65 -0
- package/src/utils/index.ts +3 -0
- 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
|
|
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
|
|
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
|
-
|
|
62
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Storage
|
|
65
|
+
const STORAGE_KEY = 'payments:withdraw';
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
//
|
|
104
|
-
const
|
|
105
|
-
const amount = form.watch('amount') || 0;
|
|
104
|
+
// Currency options
|
|
105
|
+
const currencyOptions = useCurrencyOptions(currencies);
|
|
106
106
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
totalFee,
|
|
118
|
-
finalAmount,
|
|
119
|
-
cryptoAmount,
|
|
132
|
+
token: selectedCurrency.token,
|
|
133
|
+
cryptoAmount: formatCryptoAmount(estimate.estimatedAmount, estimate.isStablecoin),
|
|
120
134
|
};
|
|
121
|
-
}, [
|
|
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
|
|
142
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
<
|
|
230
|
+
<CurrencyCombobox
|
|
216
231
|
options={currencyOptions}
|
|
217
232
|
value={field.value}
|
|
218
|
-
|
|
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
|
|
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
|
-
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<div className="text-
|
|
280
|
-
<
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
289
|
-
|
|
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={
|
|
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
|
+
}
|
package/src/components/index.ts
CHANGED
|
@@ -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';
|