@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,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
+ }