@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
|
* 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
|
|
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
|
|
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:
|
|
72
|
-
currency:
|
|
89
|
+
amount: saved.amount,
|
|
90
|
+
currency: saved.currency,
|
|
73
91
|
},
|
|
74
92
|
});
|
|
75
93
|
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// Set default currency
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
117
|
-
|
|
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 (
|
|
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
|
|
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
|
-
<
|
|
230
|
+
<CurrencyCombobox
|
|
185
231
|
options={currencyOptions}
|
|
186
232
|
value={field.value}
|
|
187
|
-
|
|
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
|
-
{/*
|
|
212
|
-
{
|
|
242
|
+
{/* Payment Breakdown */}
|
|
243
|
+
{selectedCurrency && watchedAmount >= 1 && (
|
|
213
244
|
<div className="bg-muted rounded-xl p-4 space-y-2">
|
|
214
|
-
|
|
215
|
-
<
|
|
216
|
-
|
|
217
|
-
<
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
//
|
|
129
|
-
const
|
|
129
|
+
// Prepare all display data before render
|
|
130
|
+
const displayData = useMemo(() => {
|
|
131
|
+
// Map status
|
|
130
132
|
const s = payment?.status?.toLowerCase();
|
|
131
|
-
|
|
132
|
-
if (s === '
|
|
133
|
-
if (s === '
|
|
134
|
-
if (s === '
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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',
|
|
181
|
-
<StatusIcon className={cn('h-6 w-6',
|
|
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">{
|
|
184
|
-
{
|
|
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"
|
|
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 &&
|
|
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 &&
|
|
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 &&
|
|
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>{
|
|
333
|
+
<span>{createdAt}</span>
|
|
272
334
|
</div>
|
|
273
335
|
</div>
|
|
274
336
|
|