@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.
- package/dist/config.cjs +5 -8
- package/dist/config.js +5 -8
- package/dist/index.cjs +1906 -1043
- package/dist/index.d.cts +644 -59
- package/dist/index.d.ts +644 -59
- package/dist/index.js +1886 -1040
- package/package.json +13 -16
- package/src/WalletPage.tsx +100 -0
- package/src/api/generated/ext_payments/CLAUDE.md +10 -4
- package/src/api/generated/ext_payments/_utils/fetchers/ext_payments__payments.ts +268 -5
- package/src/api/generated/ext_payments/_utils/hooks/ext_payments__payments.ts +102 -3
- package/src/api/generated/ext_payments/_utils/schemas/Balance.schema.ts +1 -1
- package/src/api/generated/ext_payments/_utils/schemas/PaginatedWithdrawalListList.schema.ts +24 -0
- package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateRequest.schema.ts +21 -0
- 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/WithdrawalCreateRequest.schema.ts +21 -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 +42 -0
- package/src/api/generated/ext_payments/_utils/schemas/WithdrawalList.schema.ts +29 -0
- package/src/api/generated/ext_payments/_utils/schemas/index.ts +8 -0
- package/src/api/generated/ext_payments/client.ts +1 -1
- package/src/api/generated/ext_payments/enums.ts +36 -0
- package/src/api/generated/ext_payments/ext_payments__payments/client.ts +104 -6
- package/src/api/generated/ext_payments/ext_payments__payments/models.ts +168 -8
- package/src/api/generated/ext_payments/index.ts +1 -1
- package/src/api/generated/ext_payments/schema.json +752 -42
- package/src/components/ActivityItem.tsx +118 -0
- package/src/components/ActivityList.tsx +93 -0
- package/src/components/AddFundsSheet.tsx +342 -0
- package/src/components/BalanceHero.tsx +102 -0
- package/src/components/CurrencyCombobox.tsx +49 -0
- package/src/components/PaymentSheet.tsx +352 -0
- package/src/components/WithdrawSheet.tsx +355 -0
- package/src/components/WithdrawalSheet.tsx +332 -0
- package/src/components/index.ts +11 -0
- package/src/config.ts +1 -0
- package/src/contexts/WalletContext.tsx +356 -0
- package/src/contexts/index.ts +13 -42
- package/src/contexts/types.ts +43 -37
- package/src/hooks/index.ts +3 -20
- 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 +9 -18
- 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/contexts/BalancesContext.tsx +0 -63
- package/src/contexts/CurrenciesContext.tsx +0 -64
- package/src/contexts/OverviewContext.tsx +0 -173
- package/src/contexts/PaymentsContext.tsx +0 -122
- package/src/contexts/PaymentsExtensionProvider.tsx +0 -56
- package/src/contexts/README.md +0 -201
- package/src/contexts/RootPaymentsContext.tsx +0 -66
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +0 -90
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +0 -274
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +0 -287
- package/src/layouts/PaymentsLayout/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/events.ts +0 -47
- package/src/layouts/PaymentsLayout/index.ts +0 -16
- package/src/layouts/PaymentsLayout/types.ts +0 -6
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +0 -121
- package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +0 -139
- package/src/layouts/PaymentsLayout/views/overview/components/index.ts +0 -2
- package/src/layouts/PaymentsLayout/views/overview/index.tsx +0 -21
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +0 -279
- package/src/layouts/PaymentsLayout/views/payments/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/payments/index.tsx +0 -18
- package/src/layouts/PaymentsLayout/views/transactions/components/TransactionsList.tsx +0 -260
- package/src/layouts/PaymentsLayout/views/transactions/components/index.ts +0 -1
- package/src/layouts/PaymentsLayout/views/transactions/index.tsx +0 -18
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity Item (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* Single row in the activity list
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { ArrowDownLeft, ArrowUpRight, Clock, CheckCircle2, XCircle, AlertCircle, Loader2 } from 'lucide-react';
|
|
10
|
+
import moment from 'moment';
|
|
11
|
+
|
|
12
|
+
import { TokenIcon } from '@djangocfg/ui-core';
|
|
13
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
14
|
+
|
|
15
|
+
import type { ActivityItem as ActivityItemType } from '../contexts/WalletContext';
|
|
16
|
+
|
|
17
|
+
interface ActivityItemProps {
|
|
18
|
+
item: ActivityItemType;
|
|
19
|
+
onClick?: () => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const statusConfig: Record<string, { icon: any; color: string; bg: string; animate?: boolean }> = {
|
|
23
|
+
pending: {
|
|
24
|
+
icon: Clock,
|
|
25
|
+
color: 'text-yellow-500',
|
|
26
|
+
bg: 'bg-yellow-500/10',
|
|
27
|
+
},
|
|
28
|
+
confirming: {
|
|
29
|
+
icon: Loader2,
|
|
30
|
+
color: 'text-blue-500',
|
|
31
|
+
bg: 'bg-blue-500/10',
|
|
32
|
+
animate: true,
|
|
33
|
+
},
|
|
34
|
+
completed: {
|
|
35
|
+
icon: CheckCircle2,
|
|
36
|
+
color: 'text-green-500',
|
|
37
|
+
bg: 'bg-green-500/10',
|
|
38
|
+
},
|
|
39
|
+
failed: {
|
|
40
|
+
icon: XCircle,
|
|
41
|
+
color: 'text-red-500',
|
|
42
|
+
bg: 'bg-red-500/10',
|
|
43
|
+
},
|
|
44
|
+
expired: {
|
|
45
|
+
icon: AlertCircle,
|
|
46
|
+
color: 'text-muted-foreground',
|
|
47
|
+
bg: 'bg-muted',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function ActivityItem({ item, onClick }: ActivityItemProps) {
|
|
52
|
+
const config = statusConfig[item.status];
|
|
53
|
+
const StatusIcon = config.icon;
|
|
54
|
+
|
|
55
|
+
const isPositive = item.type === 'payment' || item.type === 'deposit';
|
|
56
|
+
const DirectionIcon = isPositive ? ArrowDownLeft : ArrowUpRight;
|
|
57
|
+
|
|
58
|
+
const relativeTime = moment(item.createdAt).fromNow();
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<button
|
|
62
|
+
onClick={onClick}
|
|
63
|
+
className={cn(
|
|
64
|
+
'w-full flex items-center gap-4 p-4 rounded-xl',
|
|
65
|
+
'cursor-pointer',
|
|
66
|
+
'hover:bg-accent active:bg-accent/80 transition-colors',
|
|
67
|
+
'text-left'
|
|
68
|
+
)}
|
|
69
|
+
>
|
|
70
|
+
{/* Icon */}
|
|
71
|
+
<div className={cn(
|
|
72
|
+
'flex items-center justify-center w-10 h-10 rounded-full',
|
|
73
|
+
isPositive ? 'bg-green-500/10' : 'bg-red-500/10'
|
|
74
|
+
)}>
|
|
75
|
+
{item.currency ? (
|
|
76
|
+
<TokenIcon symbol={item.currency} size={24} />
|
|
77
|
+
) : (
|
|
78
|
+
<DirectionIcon className={cn(
|
|
79
|
+
'h-5 w-5',
|
|
80
|
+
isPositive ? 'text-green-500' : 'text-red-500'
|
|
81
|
+
)} />
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{/* Content */}
|
|
86
|
+
<div className="flex-1 min-w-0">
|
|
87
|
+
<div className="flex items-center gap-2">
|
|
88
|
+
<span className="font-medium truncate">{item.description}</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
|
91
|
+
<span>{relativeTime}</span>
|
|
92
|
+
{item.status !== 'completed' && (
|
|
93
|
+
<>
|
|
94
|
+
<span>·</span>
|
|
95
|
+
<span className={cn('flex items-center gap-1', config.color)}>
|
|
96
|
+
<StatusIcon className={cn('h-3 w-3', config.animate && 'animate-spin')} />
|
|
97
|
+
{item.statusDisplay}
|
|
98
|
+
</span>
|
|
99
|
+
</>
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Amount */}
|
|
105
|
+
<div className="text-right">
|
|
106
|
+
<span className={cn(
|
|
107
|
+
'font-semibold tabular-nums',
|
|
108
|
+
isPositive ? 'text-green-600 dark:text-green-400' : 'text-foreground'
|
|
109
|
+
)}>
|
|
110
|
+
{item.amountDisplay}
|
|
111
|
+
</span>
|
|
112
|
+
{item.status === 'completed' && (
|
|
113
|
+
<CheckCircle2 className="h-4 w-4 text-green-500 ml-2 inline-block" />
|
|
114
|
+
)}
|
|
115
|
+
</div>
|
|
116
|
+
</button>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity List (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* List of recent activity (payments + transactions)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { History, ChevronRight } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Button, Skeleton } from '@djangocfg/ui-core';
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
|
|
14
|
+
import { useWallet, type ActivityItem as ActivityItemType } from '../contexts/WalletContext';
|
|
15
|
+
import { ActivityItem } from './ActivityItem';
|
|
16
|
+
|
|
17
|
+
interface ActivityListProps {
|
|
18
|
+
onItemClick?: (item: ActivityItemType) => void;
|
|
19
|
+
onViewAll?: () => void;
|
|
20
|
+
limit?: number;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function ActivityList({
|
|
25
|
+
onItemClick,
|
|
26
|
+
onViewAll,
|
|
27
|
+
limit = 10,
|
|
28
|
+
className,
|
|
29
|
+
}: ActivityListProps) {
|
|
30
|
+
const { activity, isLoadingActivity, hasMoreActivity } = useWallet();
|
|
31
|
+
|
|
32
|
+
const displayedActivity = limit ? activity.slice(0, limit) : activity;
|
|
33
|
+
|
|
34
|
+
if (isLoadingActivity) {
|
|
35
|
+
return (
|
|
36
|
+
<div className={cn('space-y-2', className)}>
|
|
37
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
38
|
+
<Skeleton className="h-5 w-32" />
|
|
39
|
+
</div>
|
|
40
|
+
{[1, 2, 3, 4, 5].map((i) => (
|
|
41
|
+
<div key={i} className="flex items-center gap-4 p-4">
|
|
42
|
+
<Skeleton className="h-10 w-10 rounded-full" />
|
|
43
|
+
<div className="flex-1 space-y-2">
|
|
44
|
+
<Skeleton className="h-4 w-32" />
|
|
45
|
+
<Skeleton className="h-3 w-24" />
|
|
46
|
+
</div>
|
|
47
|
+
<Skeleton className="h-5 w-16" />
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (activity.length === 0) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={cn('text-center py-12', className)}>
|
|
57
|
+
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-muted mb-4">
|
|
58
|
+
<History className="h-8 w-8 text-muted-foreground" />
|
|
59
|
+
</div>
|
|
60
|
+
<h3 className="font-semibold mb-1">No Activity Yet</h3>
|
|
61
|
+
<p className="text-sm text-muted-foreground">
|
|
62
|
+
Your transactions will appear here
|
|
63
|
+
</p>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn('pt-6', className)}>
|
|
70
|
+
{/* Header */}
|
|
71
|
+
<div className="flex items-center justify-between px-4 py-2">
|
|
72
|
+
<h2 className="font-semibold text-lg">Recent Activity</h2>
|
|
73
|
+
{hasMoreActivity && onViewAll && (
|
|
74
|
+
<Button variant="ghost" size="sm" onClick={onViewAll} className="text-primary">
|
|
75
|
+
View All
|
|
76
|
+
<ChevronRight className="h-4 w-4 ml-1" />
|
|
77
|
+
</Button>
|
|
78
|
+
)}
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
{/* List */}
|
|
82
|
+
<div className="divide-y divide-border/50">
|
|
83
|
+
{displayedActivity.map((item) => (
|
|
84
|
+
<ActivityItem
|
|
85
|
+
key={item.id}
|
|
86
|
+
item={item}
|
|
87
|
+
onClick={() => onItemClick?.(item)}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Add Funds Sheet (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* Responsive: Dialog on desktop, Drawer on mobile
|
|
5
|
+
* Fetches real-time exchange rates on-demand
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use client';
|
|
9
|
+
|
|
10
|
+
import { useState, useMemo, useCallback } from 'react';
|
|
11
|
+
import { RefreshCw, 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 { useEstimate, useCurrencyOptions, useDefaultCurrency, useAutoSave } from '../hooks';
|
|
38
|
+
import { formatUsdRate, formatCryptoAmount, formatUsdAmount, extractErrorMessage } from '../utils';
|
|
39
|
+
import { CurrencyCombobox } from './CurrencyCombobox';
|
|
40
|
+
import type { PaymentDetail } from '../contexts/types';
|
|
41
|
+
|
|
42
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
// Schema
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const AddFundsSchema = z.object({
|
|
47
|
+
amount: z.number().min(1, 'Minimum $1'),
|
|
48
|
+
currency: z.string().min(1, 'Select a currency'),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
type AddFundsForm = z.infer<typeof AddFundsSchema>;
|
|
52
|
+
|
|
53
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
54
|
+
// Props
|
|
55
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
interface AddFundsSheetProps {
|
|
58
|
+
open: boolean;
|
|
59
|
+
onOpenChange: (open: boolean) => void;
|
|
60
|
+
onSuccess?: (payment: PaymentDetail) => void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Component
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
// Storage
|
|
68
|
+
const STORAGE_KEY = 'payments:addFunds';
|
|
69
|
+
|
|
70
|
+
interface AddFundsSaved {
|
|
71
|
+
currency: string;
|
|
72
|
+
amount: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetProps) {
|
|
76
|
+
const { currencies, isLoadingCurrencies, addFunds } = useWallet();
|
|
77
|
+
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
78
|
+
const [error, setError] = useState<string | null>(null);
|
|
79
|
+
|
|
80
|
+
// Remember last used currency and amount
|
|
81
|
+
const [saved, setSaved] = useLocalStorage<AddFundsSaved>(STORAGE_KEY, {
|
|
82
|
+
currency: '',
|
|
83
|
+
amount: 100,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const form = useForm<AddFundsForm>({
|
|
87
|
+
resolver: zodResolver(AddFundsSchema),
|
|
88
|
+
defaultValues: {
|
|
89
|
+
amount: saved.amount,
|
|
90
|
+
currency: saved.currency,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const watchedAmount = form.watch('amount');
|
|
95
|
+
const watchedCurrency = form.watch('currency');
|
|
96
|
+
|
|
97
|
+
// Auto-save to localStorage
|
|
98
|
+
useAutoSave(watchedAmount, (v: number) => setSaved(prev => ({ ...prev, amount: v })), (v) => v > 0);
|
|
99
|
+
useAutoSave(watchedCurrency, (v: string) => setSaved(prev => ({ ...prev, currency: v })));
|
|
100
|
+
|
|
101
|
+
// Currency options
|
|
102
|
+
const currencyOptions = useCurrencyOptions(currencies);
|
|
103
|
+
|
|
104
|
+
// Set default currency
|
|
105
|
+
useDefaultCurrency({
|
|
106
|
+
currencyOptions,
|
|
107
|
+
savedCurrency: saved.currency,
|
|
108
|
+
currentValue: watchedCurrency,
|
|
109
|
+
setValue: (v) => form.setValue('currency', v),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Fetch estimate with debounce
|
|
113
|
+
const { estimate, isLoading: isLoadingEstimate } = useEstimate({
|
|
114
|
+
currencyCode: watchedCurrency,
|
|
115
|
+
amountUsd: watchedAmount,
|
|
116
|
+
minAmount: 1,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Get selected currency for display
|
|
120
|
+
const selectedCurrency = currencyOptions.find(c => c.value === watchedCurrency);
|
|
121
|
+
|
|
122
|
+
// Prepare display data with fee breakdown
|
|
123
|
+
const displayData = useMemo(() => {
|
|
124
|
+
if (!selectedCurrency || !estimate) return null;
|
|
125
|
+
|
|
126
|
+
const token = selectedCurrency.token;
|
|
127
|
+
const cryptoAmount = formatCryptoAmount(estimate.estimatedAmount, estimate.isStablecoin);
|
|
128
|
+
const rate = formatUsdRate(estimate.usdRate);
|
|
129
|
+
const belowMinimum = estimate.minAmountUsd ? watchedAmount < estimate.minAmountUsd : false;
|
|
130
|
+
const minAmount = estimate.minAmountUsd ? formatUsdAmount(estimate.minAmountUsd) : undefined;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
token,
|
|
134
|
+
cryptoAmount,
|
|
135
|
+
rate,
|
|
136
|
+
showRate: !estimate.isStablecoin && estimate.usdRate > 0,
|
|
137
|
+
belowMinimum,
|
|
138
|
+
minAmount,
|
|
139
|
+
// Fee breakdown from API
|
|
140
|
+
amountToReceive: estimate.amountToReceive,
|
|
141
|
+
serviceFeeUsd: estimate.serviceFeeUsd,
|
|
142
|
+
serviceFeePercent: estimate.serviceFeePercent,
|
|
143
|
+
totalToPayUsd: estimate.totalToPayUsd,
|
|
144
|
+
};
|
|
145
|
+
}, [selectedCurrency, estimate, watchedAmount]);
|
|
146
|
+
|
|
147
|
+
// Handle submit
|
|
148
|
+
const handleSubmit = useCallback(async (data: AddFundsForm) => {
|
|
149
|
+
try {
|
|
150
|
+
setIsSubmitting(true);
|
|
151
|
+
setError(null);
|
|
152
|
+
|
|
153
|
+
const result = await addFunds({
|
|
154
|
+
amount_usd: String(data.amount),
|
|
155
|
+
currency_code: data.currency,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
form.reset();
|
|
159
|
+
onOpenChange(false);
|
|
160
|
+
onSuccess?.(result);
|
|
161
|
+
} catch (err) {
|
|
162
|
+
setError(extractErrorMessage(err, 'Failed to create payment'));
|
|
163
|
+
} finally {
|
|
164
|
+
setIsSubmitting(false);
|
|
165
|
+
}
|
|
166
|
+
}, [addFunds, form, onOpenChange, onSuccess]);
|
|
167
|
+
|
|
168
|
+
// Reset on open/close
|
|
169
|
+
const handleOpenChange = useCallback((open: boolean) => {
|
|
170
|
+
if (open) {
|
|
171
|
+
// Reset form with saved values on open
|
|
172
|
+
form.reset({
|
|
173
|
+
amount: saved.amount,
|
|
174
|
+
currency: saved.currency,
|
|
175
|
+
});
|
|
176
|
+
} else {
|
|
177
|
+
setError(null);
|
|
178
|
+
}
|
|
179
|
+
onOpenChange(open);
|
|
180
|
+
}, [form, onOpenChange, saved]);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<ResponsiveSheet open={open} onOpenChange={handleOpenChange}>
|
|
184
|
+
<ResponsiveSheetContent className="sm:max-w-md">
|
|
185
|
+
<ResponsiveSheetHeader>
|
|
186
|
+
<ResponsiveSheetTitle>Add Funds</ResponsiveSheetTitle>
|
|
187
|
+
<ResponsiveSheetDescription>
|
|
188
|
+
Add funds to your wallet using cryptocurrency
|
|
189
|
+
</ResponsiveSheetDescription>
|
|
190
|
+
</ResponsiveSheetHeader>
|
|
191
|
+
|
|
192
|
+
<Form {...form}>
|
|
193
|
+
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-6 p-4 sm:p-0 sm:mt-4">
|
|
194
|
+
{/* Amount Input */}
|
|
195
|
+
<FormField
|
|
196
|
+
control={form.control}
|
|
197
|
+
name="amount"
|
|
198
|
+
render={({ field }) => (
|
|
199
|
+
<FormItem>
|
|
200
|
+
<FormLabel>Amount (USD)</FormLabel>
|
|
201
|
+
<FormControl>
|
|
202
|
+
<div className="relative">
|
|
203
|
+
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground text-lg">
|
|
204
|
+
$
|
|
205
|
+
</span>
|
|
206
|
+
<Input
|
|
207
|
+
type="number"
|
|
208
|
+
step="0.01"
|
|
209
|
+
min="1"
|
|
210
|
+
placeholder="100"
|
|
211
|
+
className="pl-8 text-2xl h-14 font-semibold"
|
|
212
|
+
{...field}
|
|
213
|
+
onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
|
|
214
|
+
/>
|
|
215
|
+
</div>
|
|
216
|
+
</FormControl>
|
|
217
|
+
<FormMessage />
|
|
218
|
+
</FormItem>
|
|
219
|
+
)}
|
|
220
|
+
/>
|
|
221
|
+
|
|
222
|
+
{/* Currency Selection */}
|
|
223
|
+
<FormField
|
|
224
|
+
control={form.control}
|
|
225
|
+
name="currency"
|
|
226
|
+
render={({ field }) => (
|
|
227
|
+
<FormItem>
|
|
228
|
+
<FormLabel>Pay with</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
|
+
{/* Payment Breakdown */}
|
|
243
|
+
{selectedCurrency && watchedAmount >= 1 && (
|
|
244
|
+
<div className="bg-muted rounded-xl p-4 space-y-2">
|
|
245
|
+
{isLoadingEstimate ? (
|
|
246
|
+
<div className="flex items-center justify-center py-2">
|
|
247
|
+
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
248
|
+
<span className="ml-2 text-sm text-muted-foreground">Getting rate...</span>
|
|
249
|
+
</div>
|
|
250
|
+
) : displayData ? (
|
|
251
|
+
<>
|
|
252
|
+
{/* Amount breakdown */}
|
|
253
|
+
<div className="flex items-center justify-between text-sm">
|
|
254
|
+
<span className="text-muted-foreground">Amount</span>
|
|
255
|
+
<span>${formatUsdAmount(displayData.amountToReceive)}</span>
|
|
256
|
+
</div>
|
|
257
|
+
<div className="flex items-center justify-between text-sm">
|
|
258
|
+
<span className="text-muted-foreground">Service fee ({displayData.serviceFeePercent}%)</span>
|
|
259
|
+
<span>+${formatUsdAmount(displayData.serviceFeeUsd)}</span>
|
|
260
|
+
</div>
|
|
261
|
+
|
|
262
|
+
{/* Rate (for non-stablecoins) */}
|
|
263
|
+
{displayData.showRate && (
|
|
264
|
+
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
|
265
|
+
<span>Rate</span>
|
|
266
|
+
<span>1 {displayData.token} = ${displayData.rate}</span>
|
|
267
|
+
</div>
|
|
268
|
+
)}
|
|
269
|
+
|
|
270
|
+
{/* You will send */}
|
|
271
|
+
<div className="border-t pt-2 mt-2">
|
|
272
|
+
<div className="flex items-center justify-between">
|
|
273
|
+
<span className="font-medium">You will send</span>
|
|
274
|
+
<div className="text-right">
|
|
275
|
+
<div className="flex items-center gap-2 justify-end">
|
|
276
|
+
<TokenIcon symbol={displayData.token} size={20} />
|
|
277
|
+
<span className="font-mono font-semibold text-lg">
|
|
278
|
+
{displayData.cryptoAmount} {displayData.token}
|
|
279
|
+
</span>
|
|
280
|
+
</div>
|
|
281
|
+
<div className="text-sm text-muted-foreground">
|
|
282
|
+
~${formatUsdAmount(displayData.totalToPayUsd)}
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
{/* You will receive */}
|
|
289
|
+
<div className="flex items-center justify-between text-sm pt-2">
|
|
290
|
+
<span className="text-muted-foreground">You will receive</span>
|
|
291
|
+
<span className="font-medium">${formatUsdAmount(displayData.amountToReceive)}</span>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
{/* Minimum warning */}
|
|
295
|
+
{displayData.belowMinimum && displayData.minAmount && (
|
|
296
|
+
<div className="text-sm text-destructive mt-2 pt-2 border-t border-destructive/20">
|
|
297
|
+
Minimum amount: ${displayData.minAmount}
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</>
|
|
301
|
+
) : (
|
|
302
|
+
<div className="text-center text-sm text-muted-foreground py-2">
|
|
303
|
+
Enter amount to see conversion
|
|
304
|
+
</div>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{/* Error */}
|
|
310
|
+
{error && (
|
|
311
|
+
<Alert variant="destructive">
|
|
312
|
+
<AlertDescription>{error}</AlertDescription>
|
|
313
|
+
</Alert>
|
|
314
|
+
)}
|
|
315
|
+
|
|
316
|
+
{/* Submit Button */}
|
|
317
|
+
<Button
|
|
318
|
+
type="submit"
|
|
319
|
+
size="lg"
|
|
320
|
+
className="w-full h-14 text-lg rounded-xl"
|
|
321
|
+
disabled={
|
|
322
|
+
isSubmitting ||
|
|
323
|
+
currencyOptions.length === 0 ||
|
|
324
|
+
isLoadingEstimate ||
|
|
325
|
+
displayData?.belowMinimum
|
|
326
|
+
}
|
|
327
|
+
>
|
|
328
|
+
{isSubmitting ? (
|
|
329
|
+
<>
|
|
330
|
+
<RefreshCw className="h-5 w-5 mr-2 animate-spin" />
|
|
331
|
+
Creating...
|
|
332
|
+
</>
|
|
333
|
+
) : (
|
|
334
|
+
'Continue'
|
|
335
|
+
)}
|
|
336
|
+
</Button>
|
|
337
|
+
</form>
|
|
338
|
+
</Form>
|
|
339
|
+
</ResponsiveSheetContent>
|
|
340
|
+
</ResponsiveSheet>
|
|
341
|
+
);
|
|
342
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Balance Hero (Apple-style)
|
|
3
|
+
*
|
|
4
|
+
* Large centered balance display with action buttons
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use client';
|
|
8
|
+
|
|
9
|
+
import { Plus, ArrowUpRight, RefreshCw } from 'lucide-react';
|
|
10
|
+
|
|
11
|
+
import { Button, Skeleton } from '@djangocfg/ui-core';
|
|
12
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
13
|
+
|
|
14
|
+
import { useWallet } from '../contexts/WalletContext';
|
|
15
|
+
|
|
16
|
+
interface BalanceHeroProps {
|
|
17
|
+
onAddFunds?: () => void;
|
|
18
|
+
onWithdraw?: () => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function BalanceHero({ onAddFunds, onWithdraw, className }: BalanceHeroProps) {
|
|
23
|
+
const { balance, balanceAmount, isLoadingBalance, refreshWallet } = useWallet();
|
|
24
|
+
|
|
25
|
+
const formattedBalance = new Intl.NumberFormat('en-US', {
|
|
26
|
+
style: 'currency',
|
|
27
|
+
currency: 'USD',
|
|
28
|
+
minimumFractionDigits: 2,
|
|
29
|
+
maximumFractionDigits: 2,
|
|
30
|
+
}).format(balanceAmount);
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className={cn('flex flex-col items-center py-16 px-4', className)}>
|
|
34
|
+
{/* Balance Display */}
|
|
35
|
+
<div className="text-center mb-6">
|
|
36
|
+
{isLoadingBalance ? (
|
|
37
|
+
<>
|
|
38
|
+
<Skeleton className="h-12 w-48 mx-auto mb-2" />
|
|
39
|
+
<Skeleton className="h-4 w-24 mx-auto" />
|
|
40
|
+
</>
|
|
41
|
+
) : (
|
|
42
|
+
<>
|
|
43
|
+
<h1 className="text-5xl font-bold tracking-tight tabular-nums">
|
|
44
|
+
{formattedBalance}
|
|
45
|
+
</h1>
|
|
46
|
+
<p className="text-muted-foreground mt-1">Available Balance</p>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{/* Action Buttons */}
|
|
52
|
+
<div className="flex items-center gap-3">
|
|
53
|
+
<Button
|
|
54
|
+
size="lg"
|
|
55
|
+
onClick={onAddFunds}
|
|
56
|
+
className="rounded-full px-6"
|
|
57
|
+
>
|
|
58
|
+
<Plus className="h-5 w-5 mr-2" />
|
|
59
|
+
Add Funds
|
|
60
|
+
</Button>
|
|
61
|
+
|
|
62
|
+
<Button
|
|
63
|
+
size="lg"
|
|
64
|
+
variant="outline"
|
|
65
|
+
onClick={onWithdraw}
|
|
66
|
+
className="rounded-full px-6"
|
|
67
|
+
>
|
|
68
|
+
<ArrowUpRight className="h-5 w-5 mr-2" />
|
|
69
|
+
Withdraw
|
|
70
|
+
</Button>
|
|
71
|
+
|
|
72
|
+
<Button
|
|
73
|
+
size="icon"
|
|
74
|
+
variant="ghost"
|
|
75
|
+
onClick={() => refreshWallet()}
|
|
76
|
+
className="rounded-full"
|
|
77
|
+
>
|
|
78
|
+
<RefreshCw className="h-4 w-4" />
|
|
79
|
+
</Button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
{/* Stats (subtle) */}
|
|
83
|
+
{balance && !isLoadingBalance && (
|
|
84
|
+
<div className="flex items-center gap-6 mt-6 text-sm text-muted-foreground">
|
|
85
|
+
<div className="text-center">
|
|
86
|
+
<span className="block font-medium text-foreground">
|
|
87
|
+
${parseFloat(balance.total_deposited || '0').toFixed(2)}
|
|
88
|
+
</span>
|
|
89
|
+
<span>Total Deposited</span>
|
|
90
|
+
</div>
|
|
91
|
+
<div className="h-8 w-px bg-border" />
|
|
92
|
+
<div className="text-center">
|
|
93
|
+
<span className="block font-medium text-foreground">
|
|
94
|
+
${parseFloat(balance.total_withdrawn || '0').toFixed(2)}
|
|
95
|
+
</span>
|
|
96
|
+
<span>Total Withdrawn</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
)}
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|