@djangocfg/ext-payments 1.0.17 → 1.0.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/config.cjs +1 -1
  2. package/dist/config.js +1 -1
  3. package/dist/index.cjs +1175 -290
  4. package/dist/index.d.cts +226 -80
  5. package/dist/index.d.ts +226 -80
  6. package/dist/index.js +1157 -255
  7. package/package.json +9 -9
  8. package/src/WalletPage.tsx +100 -0
  9. package/src/api/generated/ext_payments/CLAUDE.md +6 -4
  10. package/src/api/generated/ext_payments/_utils/fetchers/ext_payments__payments.ts +37 -6
  11. package/src/api/generated/ext_payments/_utils/hooks/ext_payments__payments.ts +34 -3
  12. package/src/api/generated/ext_payments/_utils/schemas/Balance.schema.ts +1 -1
  13. package/src/api/generated/ext_payments/_utils/schemas/PaymentCreateResponse.schema.ts +22 -0
  14. package/src/api/generated/ext_payments/_utils/schemas/PaymentDetail.schema.ts +3 -3
  15. package/src/api/generated/ext_payments/_utils/schemas/PaymentList.schema.ts +2 -2
  16. package/src/api/generated/ext_payments/_utils/schemas/Transaction.schema.ts +1 -1
  17. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCancelResponse.schema.ts +22 -0
  18. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalCreateResponse.schema.ts +22 -0
  19. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalDetail.schema.ts +5 -5
  20. package/src/api/generated/ext_payments/_utils/schemas/WithdrawalList.schema.ts +2 -2
  21. package/src/api/generated/ext_payments/_utils/schemas/index.ts +3 -0
  22. package/src/api/generated/ext_payments/client.ts +1 -1
  23. package/src/api/generated/ext_payments/ext_payments__payments/client.ts +49 -4
  24. package/src/api/generated/ext_payments/ext_payments__payments/models.ts +33 -14
  25. package/src/api/generated/ext_payments/index.ts +1 -1
  26. package/src/api/generated/ext_payments/schema.json +167 -33
  27. package/src/components/AddFundsSheet.tsx +157 -73
  28. package/src/components/CurrencyCombobox.tsx +49 -0
  29. package/src/components/PaymentSheet.tsx +94 -32
  30. package/src/components/WithdrawSheet.tsx +121 -95
  31. package/src/components/WithdrawalSheet.tsx +332 -0
  32. package/src/components/index.ts +1 -8
  33. package/src/config.ts +1 -0
  34. package/src/contexts/WalletContext.tsx +10 -9
  35. package/src/contexts/index.ts +5 -1
  36. package/src/contexts/types.ts +46 -0
  37. package/src/hooks/index.ts +3 -0
  38. package/src/hooks/useCurrencyOptions.ts +79 -0
  39. package/src/hooks/useEstimate.ts +113 -0
  40. package/src/hooks/useWithdrawalEstimate.ts +117 -0
  41. package/src/index.ts +3 -0
  42. package/src/types/index.ts +78 -0
  43. package/src/utils/errors.ts +36 -0
  44. package/src/utils/format.ts +65 -0
  45. package/src/utils/index.ts +3 -0
  46. package/src/components/ResponsiveSheet.tsx +0 -151
@@ -2,12 +2,13 @@
2
2
  * Add Funds Sheet (Apple-style)
3
3
  *
4
4
  * Responsive: Dialog on desktop, Drawer on mobile
5
+ * Fetches real-time exchange rates on-demand
5
6
  */
6
7
 
7
8
  'use client';
8
9
 
9
10
  import { useState, useMemo, useCallback } from 'react';
10
- import { RefreshCw } from 'lucide-react';
11
+ import { RefreshCw, Loader2 } from 'lucide-react';
11
12
  import { useForm } from 'react-hook-form';
12
13
  import { z } from 'zod';
13
14
  import { zodResolver } from '@hookform/resolvers/zod';
@@ -16,7 +17,6 @@ import {
16
17
  Alert,
17
18
  AlertDescription,
18
19
  Button,
19
- Combobox,
20
20
  Form,
21
21
  FormControl,
22
22
  FormField,
@@ -30,17 +30,21 @@ import {
30
30
  ResponsiveSheetDescription,
31
31
  ResponsiveSheetHeader,
32
32
  ResponsiveSheetTitle,
33
+ useLocalStorage,
33
34
  } from '@djangocfg/ui-core';
34
35
 
35
36
  import { useWallet } from '../contexts/WalletContext';
36
- import type { PaymentDetail } from '../api/generated/ext_payments/_utils/schemas';
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';
37
41
 
38
42
  // ─────────────────────────────────────────────────────────────────────────────
39
43
  // Schema
40
44
  // ─────────────────────────────────────────────────────────────────────────────
41
45
 
42
46
  const AddFundsSchema = z.object({
43
- amount: z.number().min(1, 'Minimum $1.00'),
47
+ amount: z.number().min(1, 'Minimum $1'),
44
48
  currency: z.string().min(1, 'Select a currency'),
45
49
  });
46
50
 
@@ -60,44 +64,85 @@ interface AddFundsSheetProps {
60
64
  // Component
61
65
  // ─────────────────────────────────────────────────────────────────────────────
62
66
 
67
+ // Storage
68
+ const STORAGE_KEY = 'payments:addFunds';
69
+
70
+ interface AddFundsSaved {
71
+ currency: string;
72
+ amount: number;
73
+ }
74
+
63
75
  export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetProps) {
64
76
  const { currencies, isLoadingCurrencies, addFunds } = useWallet();
65
77
  const [isSubmitting, setIsSubmitting] = useState(false);
66
78
  const [error, setError] = useState<string | null>(null);
67
79
 
80
+ // Remember last used currency and amount
81
+ const [saved, setSaved] = useLocalStorage<AddFundsSaved>(STORAGE_KEY, {
82
+ currency: '',
83
+ amount: 100,
84
+ });
85
+
68
86
  const form = useForm<AddFundsForm>({
69
87
  resolver: zodResolver(AddFundsSchema),
70
88
  defaultValues: {
71
- amount: 100,
72
- currency: '',
89
+ amount: saved.amount,
90
+ currency: saved.currency,
73
91
  },
74
92
  });
75
93
 
76
- // Currency options for combobox
77
- const currencyOptions = useMemo(() => {
78
- return currencies.map((c) => ({
79
- value: c.code,
80
- label: c.network ? `${c.code} (${c.network})` : c.code,
81
- rate: c.rate,
82
- network: c.network,
83
- }));
84
- }, [currencies]);
85
-
86
- // Set default currency when loaded
87
- useMemo(() => {
88
- if (currencyOptions.length > 0 && !form.getValues('currency')) {
89
- const usdt = currencyOptions.find(c => c.value.includes('USDT'));
90
- form.setValue('currency', usdt?.value || currencyOptions[0].value);
91
- }
92
- }, [currencyOptions, form]);
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;
93
131
 
94
- // Calculate crypto amount
95
- const selectedCurrency = currencyOptions.find(c => c.value === form.watch('currency'));
96
- const cryptoAmount = useMemo(() => {
97
- const amount = form.watch('amount');
98
- if (!selectedCurrency?.rate || !amount) return null;
99
- return amount / selectedCurrency.rate;
100
- }, [form.watch('amount'), selectedCurrency]);
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]);
101
146
 
102
147
  // Handle submit
103
148
  const handleSubmit = useCallback(async (data: AddFundsForm) => {
@@ -113,25 +158,26 @@ export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetPr
113
158
  form.reset();
114
159
  onOpenChange(false);
115
160
  onSuccess?.(result);
116
- } catch (err: any) {
117
- const message = err?.response?.data?.message
118
- || err?.response?.data?.detail
119
- || err?.message
120
- || 'Failed to create payment';
121
- setError(message);
161
+ } catch (err) {
162
+ setError(extractErrorMessage(err, 'Failed to create payment'));
122
163
  } finally {
123
164
  setIsSubmitting(false);
124
165
  }
125
166
  }, [addFunds, form, onOpenChange, onSuccess]);
126
167
 
127
- // Reset on close
168
+ // Reset on open/close
128
169
  const handleOpenChange = useCallback((open: boolean) => {
129
- if (!open) {
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 {
130
177
  setError(null);
131
- form.reset();
132
178
  }
133
179
  onOpenChange(open);
134
- }, [form, onOpenChange]);
180
+ }, [form, onOpenChange, saved]);
135
181
 
136
182
  return (
137
183
  <ResponsiveSheet open={open} onOpenChange={handleOpenChange}>
@@ -161,7 +207,7 @@ export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetPr
161
207
  type="number"
162
208
  step="0.01"
163
209
  min="1"
164
- placeholder="100.00"
210
+ placeholder="100"
165
211
  className="pl-8 text-2xl h-14 font-semibold"
166
212
  {...field}
167
213
  onChange={(e) => field.onChange(parseFloat(e.target.value) || 0)}
@@ -181,26 +227,11 @@ export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetPr
181
227
  <FormItem>
182
228
  <FormLabel>Pay with</FormLabel>
183
229
  <FormControl>
184
- <Combobox
230
+ <CurrencyCombobox
185
231
  options={currencyOptions}
186
232
  value={field.value}
187
- onValueChange={field.onChange}
188
- placeholder="Select currency..."
189
- searchPlaceholder="Search..."
233
+ onChange={field.onChange}
190
234
  disabled={isLoadingCurrencies}
191
- className="h-14"
192
- renderOption={(option) => (
193
- <div className="flex items-center gap-3 flex-1">
194
- <TokenIcon symbol={option.value} size={24} />
195
- <span className="font-medium">{option.label}</span>
196
- </div>
197
- )}
198
- renderValue={(option) => option && (
199
- <div className="flex items-center gap-3">
200
- <TokenIcon symbol={option.value} size={24} />
201
- <span className="font-medium">{option.label}</span>
202
- </div>
203
- )}
204
235
  />
205
236
  </FormControl>
206
237
  <FormMessage />
@@ -208,22 +239,70 @@ export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetPr
208
239
  )}
209
240
  />
210
241
 
211
- {/* Conversion Preview */}
212
- {cryptoAmount !== null && selectedCurrency && (
242
+ {/* Payment Breakdown */}
243
+ {selectedCurrency && watchedAmount >= 1 && (
213
244
  <div className="bg-muted rounded-xl p-4 space-y-2">
214
- <div className="flex items-center justify-between">
215
- <span className="text-muted-foreground">You will send</span>
216
- <div className="flex items-center gap-2">
217
- <TokenIcon symbol={selectedCurrency.value} size={20} />
218
- <span className="font-mono font-semibold">
219
- {cryptoAmount.toFixed(8)} {selectedCurrency.value}
220
- </span>
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
221
304
  </div>
222
- </div>
223
- <div className="flex items-center justify-between text-sm text-muted-foreground">
224
- <span>Rate</span>
225
- <span>1 {selectedCurrency.value} = ${selectedCurrency.rate.toFixed(2)}</span>
226
- </div>
305
+ )}
227
306
  </div>
228
307
  )}
229
308
 
@@ -239,7 +318,12 @@ export function AddFundsSheet({ open, onOpenChange, onSuccess }: AddFundsSheetPr
239
318
  type="submit"
240
319
  size="lg"
241
320
  className="w-full h-14 text-lg rounded-xl"
242
- disabled={isSubmitting || currencyOptions.length === 0}
321
+ disabled={
322
+ isSubmitting ||
323
+ currencyOptions.length === 0 ||
324
+ isLoadingEstimate ||
325
+ displayData?.belowMinimum
326
+ }
243
327
  >
244
328
  {isSubmitting ? (
245
329
  <>
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ /**
4
+ * Currency Combobox with TokenIcon
5
+ */
6
+
7
+ import { Combobox, TokenIcon } from '@djangocfg/ui-core';
8
+ import type { CurrencyOption } from '../types';
9
+
10
+ interface CurrencyComboboxProps {
11
+ options: CurrencyOption[];
12
+ value: string;
13
+ onChange: (value: string) => void;
14
+ disabled?: boolean;
15
+ placeholder?: string;
16
+ label?: string;
17
+ }
18
+
19
+ export function CurrencyCombobox({
20
+ options,
21
+ value,
22
+ onChange,
23
+ disabled,
24
+ placeholder = 'Select currency...',
25
+ }: CurrencyComboboxProps) {
26
+ return (
27
+ <Combobox
28
+ options={options}
29
+ value={value}
30
+ onValueChange={onChange}
31
+ placeholder={placeholder}
32
+ searchPlaceholder="Search..."
33
+ disabled={disabled}
34
+ className="h-14"
35
+ renderOption={(option) => (
36
+ <div className="flex items-center gap-3 flex-1">
37
+ <TokenIcon symbol={option.value} size={24} />
38
+ <span className="font-medium">{option.label}</span>
39
+ </div>
40
+ )}
41
+ renderValue={(option) => option && (
42
+ <div className="flex items-center gap-3">
43
+ <TokenIcon symbol={option.value} size={24} />
44
+ <span className="font-medium">{option.label}</span>
45
+ </div>
46
+ )}
47
+ />
48
+ );
49
+ }
@@ -43,6 +43,7 @@ interface PaymentSheetProps {
43
43
  paymentId: string | null;
44
44
  open: boolean;
45
45
  onOpenChange: (open: boolean) => void;
46
+ onCreateNew?: () => void;
46
47
  }
47
48
 
48
49
  // ─────────────────────────────────────────────────────────────────────────────
@@ -87,7 +88,7 @@ const statusConfig: Record<string, { icon: any; color: string; bg: string; label
87
88
  // Component
88
89
  // ─────────────────────────────────────────────────────────────────────────────
89
90
 
90
- export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProps) {
91
+ export function PaymentSheet({ paymentId, open, onOpenChange, onCreateNew }: PaymentSheetProps) {
91
92
  const { getPaymentDetails } = useWallet();
92
93
  const [timeLeft, setTimeLeft] = useState<string>('');
93
94
 
@@ -125,34 +126,83 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
125
126
  return () => clearInterval(interval);
126
127
  }, [payment?.expires_at]);
127
128
 
128
- // Map status
129
- const status = useMemo(() => {
129
+ // Prepare all display data before render
130
+ const displayData = useMemo(() => {
131
+ // Map status
130
132
  const s = payment?.status?.toLowerCase();
131
- if (s === 'completed' || s === 'success' || s === 'finished') return 'completed';
132
- if (s === 'confirming' || s === 'partially_paid') return 'confirming';
133
- if (s === 'expired') return 'expired';
134
- if (s === 'failed' || s === 'error' || s === 'cancelled') return 'failed';
135
- return 'pending';
136
- }, [payment?.status]);
137
-
138
- const config = statusConfig[status];
139
- const StatusIcon = config.icon;
133
+ let status: string;
134
+ if (s === 'completed' || s === 'success' || s === 'finished') status = 'completed';
135
+ else if (s === 'confirming' || s === 'partially_paid') status = 'confirming';
136
+ else if (s === 'expired') status = 'expired';
137
+ else if (s === 'failed' || s === 'error' || s === 'cancelled') status = 'failed';
138
+ else status = 'pending';
139
+
140
+ const config = statusConfig[status];
141
+ const isPending = status === 'pending';
142
+ const isExpired = status === 'expired' || timeLeft === 'Expired';
143
+ const isCompleted = status === 'completed';
144
+ const isFailed = status === 'failed';
145
+ const isConfirming = status === 'confirming';
146
+ const canPay = isPending && !isExpired;
147
+
148
+ // Description text
149
+ let description = '';
150
+ if (canPay) description = 'Send cryptocurrency to complete payment';
151
+ else if (isExpired) description = 'This payment has expired';
152
+ else if (isCompleted) description = 'Payment completed successfully';
153
+ else if (isFailed) description = 'Payment failed';
154
+ else if (isConfirming) description = 'Confirming your payment';
155
+
156
+ // Status badge
157
+ const statusBadge = {
158
+ bg: isExpired ? 'bg-muted' : config.bg,
159
+ iconColor: isExpired ? 'text-muted-foreground' : config.color,
160
+ iconAnimate: config.animate,
161
+ label: isExpired ? 'Payment Expired' : config.label,
162
+ subtitle: canPay && timeLeft
163
+ ? `Expires in ${timeLeft}`
164
+ : isExpired
165
+ ? 'Please create a new payment to continue'
166
+ : null,
167
+ };
140
168
 
141
- // QR code URL
142
- const qrCodeUrl = payment?.pay_address
143
- ? `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(payment.pay_address)}`
144
- : null;
169
+ // QR code URL
170
+ const qrCodeUrl = payment?.pay_address && canPay
171
+ ? `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(payment.pay_address)}`
172
+ : null;
173
+
174
+ // Formatted values
175
+ const amountUsd = payment?.amount_usd ? `$${parseFloat(payment.amount_usd).toFixed(2)} USD` : '';
176
+ const createdAt = payment?.created_at
177
+ ? moment.utc(payment.created_at).local().format('MMM D, YYYY HH:mm')
178
+ : '';
179
+
180
+ return {
181
+ status,
182
+ config,
183
+ isPending,
184
+ isExpired,
185
+ isCompleted,
186
+ isFailed,
187
+ isConfirming,
188
+ canPay,
189
+ description,
190
+ statusBadge,
191
+ qrCodeUrl,
192
+ amountUsd,
193
+ createdAt,
194
+ };
195
+ }, [payment, timeLeft]);
145
196
 
146
- const isPending = status === 'pending';
197
+ const { config, canPay, isExpired, description, statusBadge, qrCodeUrl, amountUsd, createdAt } = displayData;
198
+ const StatusIcon = config.icon;
147
199
 
148
200
  return (
149
201
  <ResponsiveSheet open={open} onOpenChange={onOpenChange}>
150
202
  <ResponsiveSheetContent className="sm:max-w-lg">
151
203
  <ResponsiveSheetHeader>
152
204
  <ResponsiveSheetTitle>Payment Details</ResponsiveSheetTitle>
153
- <ResponsiveSheetDescription>
154
- {isPending ? 'Send cryptocurrency to complete payment' : 'Payment information'}
155
- </ResponsiveSheetDescription>
205
+ <ResponsiveSheetDescription>{description}</ResponsiveSheetDescription>
156
206
  </ResponsiveSheetHeader>
157
207
 
158
208
  <div className="p-4 sm:p-0 sm:mt-4 overflow-y-auto max-h-[70vh]">
@@ -177,14 +227,12 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
177
227
  {payment && !isLoading && (
178
228
  <div className="space-y-6">
179
229
  {/* Status Badge */}
180
- <div className={cn('flex items-center gap-3 p-4 rounded-xl', config.bg)}>
181
- <StatusIcon className={cn('h-6 w-6', config.color, config.animate && 'animate-spin')} />
230
+ <div className={cn('flex items-center gap-3 p-4 rounded-xl', statusBadge.bg)}>
231
+ <StatusIcon className={cn('h-6 w-6', statusBadge.iconColor, statusBadge.iconAnimate && 'animate-spin')} />
182
232
  <div className="flex-1">
183
- <div className="font-semibold">{config.label}</div>
184
- {isPending && timeLeft && (
185
- <div className="text-sm text-muted-foreground">
186
- Expires in {timeLeft}
187
- </div>
233
+ <div className="font-semibold">{statusBadge.label}</div>
234
+ {statusBadge.subtitle && (
235
+ <div className="text-sm text-muted-foreground">{statusBadge.subtitle}</div>
188
236
  )}
189
237
  </div>
190
238
  </div>
@@ -202,7 +250,7 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
202
250
  </div>
203
251
  <div className="flex items-center justify-between text-sm">
204
252
  <span className="text-muted-foreground">Equivalent</span>
205
- <span className="font-semibold">${parseFloat(payment.amount_usd).toFixed(2)} USD</span>
253
+ <span className="font-semibold">{amountUsd}</span>
206
254
  </div>
207
255
  {payment.currency_network && (
208
256
  <div className="flex items-center justify-between text-sm pt-2 border-t">
@@ -213,14 +261,14 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
213
261
  </div>
214
262
 
215
263
  {/* QR Code */}
216
- {qrCodeUrl && isPending && (
264
+ {qrCodeUrl && (
217
265
  <div className="flex justify-center p-6 bg-white rounded-xl">
218
266
  <img src={qrCodeUrl} alt="Payment QR Code" className="w-48 h-48" />
219
267
  </div>
220
268
  )}
221
269
 
222
270
  {/* Payment Address */}
223
- {payment.pay_address && isPending && (
271
+ {payment.pay_address && canPay && (
224
272
  <div className="space-y-2">
225
273
  <label className="text-sm font-medium">Payment Address</label>
226
274
  <div className="flex items-center gap-2">
@@ -232,6 +280,20 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
232
280
  </div>
233
281
  )}
234
282
 
283
+ {/* Expired - Create New Payment */}
284
+ {isExpired && onCreateNew && (
285
+ <Button
286
+ size="lg"
287
+ className="w-full"
288
+ onClick={() => {
289
+ onOpenChange(false);
290
+ onCreateNew();
291
+ }}
292
+ >
293
+ Create New Payment
294
+ </Button>
295
+ )}
296
+
235
297
  {/* Transaction Hash (if completed) */}
236
298
  {payment.transaction_hash && (
237
299
  <div className="space-y-2">
@@ -243,7 +305,7 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
243
305
  )}
244
306
 
245
307
  {/* External Link */}
246
- {payment.payment_url && isPending && (
308
+ {payment.payment_url && canPay && (
247
309
  <Button
248
310
  variant="outline"
249
311
  className="w-full"
@@ -268,7 +330,7 @@ export function PaymentSheet({ paymentId, open, onOpenChange }: PaymentSheetProp
268
330
  )}
269
331
  <div className="flex justify-between">
270
332
  <span>Created</span>
271
- <span>{moment.utc(payment.created_at).local().format('MMM D, YYYY HH:mm')}</span>
333
+ <span>{createdAt}</span>
272
334
  </div>
273
335
  </div>
274
336