@djangocfg/layouts 1.0.2 → 1.0.3
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/package.json +5 -5
- package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +5 -3
- package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +3 -3
- package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +20 -4
- package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +41 -57
- package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +163 -54
- package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +254 -0
- package/src/layouts/PaymentsLayout/components/index.ts +1 -0
- package/src/layouts/PaymentsLayout/context/RootPaymentsContext.tsx +134 -0
- package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +6 -6
- package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +8 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Layout system and components for Unrealon applications",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "DjangoCFG",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"check": "tsc --noEmit"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
+
"@djangocfg/api": "^1.0.3",
|
|
57
|
+
"@djangocfg/og-image": "^1.0.3",
|
|
58
|
+
"@djangocfg/ui": "^1.0.3",
|
|
56
59
|
"@hookform/resolvers": "^5.2.0",
|
|
57
|
-
"@djangocfg/api": "^1.0.2",
|
|
58
|
-
"@djangocfg/og-image": "^1.0.2",
|
|
59
|
-
"@djangocfg/ui": "^1.0.2",
|
|
60
60
|
"consola": "^3.4.2",
|
|
61
61
|
"lucide-react": "^0.468.0",
|
|
62
62
|
"next": "^15.4.4",
|
|
@@ -76,10 +76,10 @@
|
|
|
76
76
|
"vidstack": "0.6.15"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
|
+
"@djangocfg/typescript-config": "^1.0.3",
|
|
79
80
|
"@types/node": "^24.7.2",
|
|
80
81
|
"@types/react": "19.2.2",
|
|
81
82
|
"@types/react-dom": "19.2.1",
|
|
82
|
-
"@djangocfg/typescript-config": "^1.0.2",
|
|
83
83
|
"eslint": "^9.37.0",
|
|
84
84
|
"typescript": "^5.9.3"
|
|
85
85
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Mail, MessageCircle } from 'lucide-react';
|
|
1
|
+
import { Mail, MessageCircle, HelpCircle } from 'lucide-react';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
|
|
4
4
|
import { Button } from '@djangocfg/ui/components';
|
|
@@ -64,7 +64,8 @@ export const AuthHelp: React.FC<AuthHelpProps> = ({
|
|
|
64
64
|
size="sm"
|
|
65
65
|
className="text-xs"
|
|
66
66
|
>
|
|
67
|
-
<a href={supportUrl} target="_blank" rel="noopener noreferrer">
|
|
67
|
+
<a href={supportUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1">
|
|
68
|
+
<HelpCircle className="w-3 h-3" />
|
|
68
69
|
Need help?
|
|
69
70
|
</a>
|
|
70
71
|
</Button>
|
|
@@ -100,7 +101,8 @@ export const AuthHelp: React.FC<AuthHelpProps> = ({
|
|
|
100
101
|
size="sm"
|
|
101
102
|
className="text-xs h-7 px-2"
|
|
102
103
|
>
|
|
103
|
-
<a href={supportUrl} target="_blank" rel="noopener noreferrer">
|
|
104
|
+
<a href={supportUrl} target="_blank" rel="noopener noreferrer" className="flex items-center gap-1">
|
|
105
|
+
<HelpCircle className="w-3 h-3" />
|
|
104
106
|
Get Help
|
|
105
107
|
</a>
|
|
106
108
|
</Button>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Mail, Phone, User } from 'lucide-react';
|
|
2
|
+
import { Mail, Phone, User, Send } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
@@ -231,7 +231,7 @@ export const IdentifierForm: React.FC = () => {
|
|
|
231
231
|
</div>
|
|
232
232
|
) : (
|
|
233
233
|
<div className="flex items-center gap-2">
|
|
234
|
-
|
|
234
|
+
<Send className="w-4 h-4" />
|
|
235
235
|
Send verification code
|
|
236
236
|
</div>
|
|
237
237
|
)}
|
|
@@ -314,7 +314,7 @@ export const IdentifierForm: React.FC = () => {
|
|
|
314
314
|
</div>
|
|
315
315
|
) : (
|
|
316
316
|
<div className="flex items-center gap-2">
|
|
317
|
-
|
|
317
|
+
<Send className="w-4 h-4" />
|
|
318
318
|
Send verification code
|
|
319
319
|
</div>
|
|
320
320
|
)}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { Mail, MessageCircle } from 'lucide-react';
|
|
2
|
+
import { Mail, MessageCircle, ArrowLeft, RotateCw, ShieldCheck } from 'lucide-react';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
Button,
|
|
@@ -113,7 +113,17 @@ export const OTPForm: React.FC = () => {
|
|
|
113
113
|
className="w-full h-11 text-base font-medium"
|
|
114
114
|
disabled={isLoading || otp.length < 6}
|
|
115
115
|
>
|
|
116
|
-
{isLoading ?
|
|
116
|
+
{isLoading ? (
|
|
117
|
+
<div className="flex items-center gap-2">
|
|
118
|
+
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin" />
|
|
119
|
+
Verifying...
|
|
120
|
+
</div>
|
|
121
|
+
) : (
|
|
122
|
+
<div className="flex items-center gap-2">
|
|
123
|
+
<ShieldCheck className="w-5 h-5" />
|
|
124
|
+
Verify Code
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
117
127
|
</Button>
|
|
118
128
|
|
|
119
129
|
<div className="flex gap-3">
|
|
@@ -124,7 +134,10 @@ export const OTPForm: React.FC = () => {
|
|
|
124
134
|
disabled={isLoading}
|
|
125
135
|
className="flex-1 h-10"
|
|
126
136
|
>
|
|
127
|
-
|
|
137
|
+
<div className="flex items-center gap-2">
|
|
138
|
+
<ArrowLeft className="w-4 h-4" />
|
|
139
|
+
Back
|
|
140
|
+
</div>
|
|
128
141
|
</Button>
|
|
129
142
|
|
|
130
143
|
<Button
|
|
@@ -134,7 +147,10 @@ export const OTPForm: React.FC = () => {
|
|
|
134
147
|
disabled={isLoading}
|
|
135
148
|
className="flex-1 h-10"
|
|
136
149
|
>
|
|
137
|
-
|
|
150
|
+
<div className="flex items-center gap-2">
|
|
151
|
+
<RotateCw className="w-4 h-4" />
|
|
152
|
+
Resend
|
|
153
|
+
</div>
|
|
138
154
|
</Button>
|
|
139
155
|
</div>
|
|
140
156
|
</div>
|
|
@@ -5,46 +5,44 @@
|
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
|
-
import React
|
|
8
|
+
import React from 'react';
|
|
9
9
|
import {
|
|
10
10
|
PaymentsProvider,
|
|
11
|
-
BalancesProvider,
|
|
12
11
|
ApiKeysProvider,
|
|
13
12
|
OverviewProvider,
|
|
13
|
+
RootPaymentsProvider,
|
|
14
14
|
} from '@djangocfg/api/cfg/contexts';
|
|
15
15
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@djangocfg/ui';
|
|
16
16
|
import { Wallet, CreditCard, History, Key, Crown } from 'lucide-react';
|
|
17
|
-
import type { PaymentTab } from './types';
|
|
18
17
|
import { OverviewView } from './views/overview';
|
|
19
18
|
import { PaymentsView } from './views/payments';
|
|
20
19
|
import { TransactionsView } from './views/transactions';
|
|
21
20
|
import { ApiKeysView } from './views/apikeys';
|
|
22
21
|
import { TariffsView } from './views/tariffs';
|
|
23
|
-
import { CreateApiKeyDialog, DeleteApiKeyDialog } from './components';
|
|
22
|
+
import { CreateApiKeyDialog, DeleteApiKeyDialog, CreatePaymentDialog, PaymentDetailsDialog } from './components';
|
|
24
23
|
|
|
25
24
|
// ─────────────────────────────────────────────────────────────────────────
|
|
26
|
-
// Payments Layout
|
|
25
|
+
// Payments Layout
|
|
27
26
|
// ─────────────────────────────────────────────────────────────────────────
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
export interface PaymentsLayoutProps {
|
|
29
|
+
children?: React.ReactNode;
|
|
30
|
+
}
|
|
31
31
|
|
|
32
|
+
export const PaymentsLayout: React.FC<PaymentsLayoutProps> = () => {
|
|
32
33
|
return (
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
<RootPaymentsProvider>
|
|
35
|
+
<div className="h-full p-6 space-y-6">
|
|
36
|
+
{/* Page Header */}
|
|
37
|
+
<div>
|
|
38
|
+
<h1 className="text-3xl font-bold tracking-tight">Payments & Billing</h1>
|
|
39
|
+
<p className="text-muted-foreground">
|
|
40
|
+
Manage your payments, subscriptions, API keys, and account balance
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
41
43
|
|
|
42
44
|
{/* Main Content with Tabs */}
|
|
43
|
-
<Tabs
|
|
44
|
-
value={activeTab}
|
|
45
|
-
onValueChange={(value) => setActiveTab(value as PaymentTab)}
|
|
46
|
-
className="space-y-6"
|
|
47
|
-
>
|
|
45
|
+
<Tabs defaultValue="overview" className="space-y-6">
|
|
48
46
|
<TabsList className="inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground">
|
|
49
47
|
<TabsTrigger value="overview" className="inline-flex items-center gap-2 px-3 py-1.5">
|
|
50
48
|
<Wallet className="h-4 w-4" />
|
|
@@ -68,58 +66,44 @@ const PaymentsLayoutContent: React.FC = () => {
|
|
|
68
66
|
</TabsTrigger>
|
|
69
67
|
</TabsList>
|
|
70
68
|
|
|
71
|
-
{/*
|
|
69
|
+
{/* Each tab wrapped in its own provider - loads only when tab is active */}
|
|
72
70
|
<TabsContent value="overview" className="space-y-6">
|
|
73
|
-
<
|
|
71
|
+
<OverviewProvider>
|
|
72
|
+
<PaymentsProvider>
|
|
73
|
+
<OverviewView />
|
|
74
|
+
<CreatePaymentDialog />
|
|
75
|
+
</PaymentsProvider>
|
|
76
|
+
</OverviewProvider>
|
|
74
77
|
</TabsContent>
|
|
75
78
|
|
|
76
|
-
{/* Payments Tab */}
|
|
77
79
|
<TabsContent value="payments" className="space-y-6">
|
|
78
|
-
<
|
|
80
|
+
<PaymentsProvider>
|
|
81
|
+
<PaymentsView />
|
|
82
|
+
<CreatePaymentDialog />
|
|
83
|
+
</PaymentsProvider>
|
|
79
84
|
</TabsContent>
|
|
80
85
|
|
|
81
|
-
{/* Transactions Tab */}
|
|
82
86
|
<TabsContent value="transactions" className="space-y-6">
|
|
83
87
|
<TransactionsView />
|
|
84
88
|
</TabsContent>
|
|
85
89
|
|
|
86
|
-
{/* API Keys Tab */}
|
|
87
90
|
<TabsContent value="apikeys" className="space-y-6">
|
|
88
|
-
<
|
|
91
|
+
<ApiKeysProvider>
|
|
92
|
+
<ApiKeysView />
|
|
93
|
+
<CreateApiKeyDialog />
|
|
94
|
+
<DeleteApiKeyDialog />
|
|
95
|
+
</ApiKeysProvider>
|
|
89
96
|
</TabsContent>
|
|
90
97
|
|
|
91
|
-
|
|
92
|
-
<
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
</Tabs>
|
|
98
|
+
<TabsContent value="tariffs" className="space-y-6">
|
|
99
|
+
<TariffsView />
|
|
100
|
+
</TabsContent>
|
|
101
|
+
</Tabs>
|
|
96
102
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
<DeleteApiKeyDialog />
|
|
103
|
+
{/* Global Payment Details Dialog */}
|
|
104
|
+
<PaymentDetailsDialog />
|
|
100
105
|
</div>
|
|
101
|
-
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
105
|
-
// Payments Layout (with providers)
|
|
106
|
-
// ─────────────────────────────────────────────────────────────────────────
|
|
107
|
-
|
|
108
|
-
export interface PaymentsLayoutProps {
|
|
109
|
-
children?: React.ReactNode;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export const PaymentsLayout: React.FC<PaymentsLayoutProps> = () => {
|
|
113
|
-
return (
|
|
114
|
-
<OverviewProvider>
|
|
115
|
-
<PaymentsProvider>
|
|
116
|
-
<BalancesProvider>
|
|
117
|
-
<ApiKeysProvider>
|
|
118
|
-
<PaymentsLayoutContent />
|
|
119
|
-
</ApiKeysProvider>
|
|
120
|
-
</BalancesProvider>
|
|
121
|
-
</PaymentsProvider>
|
|
122
|
-
</OverviewProvider>
|
|
106
|
+
</RootPaymentsProvider>
|
|
123
107
|
);
|
|
124
108
|
};
|
|
125
109
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
'use client';
|
|
7
7
|
|
|
8
|
-
import React, { useState } from 'react';
|
|
8
|
+
import React, { useState, useEffect, useMemo } from 'react';
|
|
9
9
|
import {
|
|
10
10
|
Dialog,
|
|
11
11
|
DialogContent,
|
|
@@ -21,21 +21,21 @@ import {
|
|
|
21
21
|
FormLabel,
|
|
22
22
|
FormMessage,
|
|
23
23
|
Input,
|
|
24
|
-
|
|
25
|
-
SelectContent,
|
|
26
|
-
SelectItem,
|
|
27
|
-
SelectTrigger,
|
|
28
|
-
SelectValue,
|
|
24
|
+
Combobox,
|
|
29
25
|
Button,
|
|
26
|
+
TokenIcon,
|
|
30
27
|
useEventListener,
|
|
31
28
|
} from '@djangocfg/ui';
|
|
32
29
|
import { Plus, RefreshCw } from 'lucide-react';
|
|
33
30
|
import { useForm } from 'react-hook-form';
|
|
34
31
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
35
|
-
import { usePaymentsContext } from '@djangocfg/api/cfg/contexts';
|
|
32
|
+
import { usePaymentsContext, useRootPaymentsContext } from '@djangocfg/api/cfg/contexts';
|
|
36
33
|
import { Schemas, Enums } from '@djangocfg/api/cfg/generated';
|
|
37
34
|
import { paymentsLogger } from '../../../utils/logger';
|
|
38
35
|
import { PAYMENTS_DIALOG_EVENTS, closePaymentsDialog } from '../events';
|
|
36
|
+
import { openPaymentDetails } from './PaymentDetailsDialog';
|
|
37
|
+
import type { ProviderCurrency } from '@djangocfg/api/cfg/contexts';
|
|
38
|
+
import type { ComboboxOption } from '@djangocfg/ui';
|
|
39
39
|
|
|
40
40
|
const { PaymentCreateRequestSchema } = Schemas;
|
|
41
41
|
type PaymentCreateRequest = Schemas.PaymentCreateRequest;
|
|
@@ -44,17 +44,65 @@ const { PaymentCreateRequestCurrencyCode, PaymentCreateRequestProvider } = Enums
|
|
|
44
44
|
export const CreatePaymentDialog: React.FC = () => {
|
|
45
45
|
const [open, setOpen] = useState(false);
|
|
46
46
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
47
|
+
|
|
47
48
|
const { createPayment } = usePaymentsContext();
|
|
49
|
+
const {
|
|
50
|
+
providerCurrencies,
|
|
51
|
+
isLoadingProviderCurrencies,
|
|
52
|
+
} = useRootPaymentsContext();
|
|
48
53
|
|
|
49
54
|
const form = useForm<PaymentCreateRequest>({
|
|
50
55
|
resolver: zodResolver(PaymentCreateRequestSchema),
|
|
51
56
|
defaultValues: {
|
|
52
57
|
amount_usd: 10,
|
|
53
58
|
currency_code: PaymentCreateRequestCurrencyCode.USDT,
|
|
54
|
-
provider: PaymentCreateRequestProvider.NOWPAYMENTS,
|
|
55
59
|
},
|
|
56
60
|
});
|
|
57
61
|
|
|
62
|
+
// Group currencies by token and create combobox options
|
|
63
|
+
const currencyOptions = useMemo((): ComboboxOption[] => {
|
|
64
|
+
if (!providerCurrencies?.results) return [];
|
|
65
|
+
|
|
66
|
+
const enabledCurrencies = providerCurrencies.results.filter(pc => pc.is_enabled);
|
|
67
|
+
|
|
68
|
+
return enabledCurrencies.map((pc) => {
|
|
69
|
+
const networkInfo = pc.network ? ` (${pc.network.name})` : '';
|
|
70
|
+
const label = `${pc.currency.code} - ${pc.currency.name}${networkInfo}`;
|
|
71
|
+
const description = pc.network ? pc.network.code : 'No network';
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
value: pc.provider_currency_code,
|
|
75
|
+
label,
|
|
76
|
+
description,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
}, [providerCurrencies]);
|
|
80
|
+
|
|
81
|
+
// Get ProviderCurrency by currency_code
|
|
82
|
+
const getProviderCurrency = (currencyCode: string) => {
|
|
83
|
+
return providerCurrencies?.results?.find(
|
|
84
|
+
pc => pc.provider_currency_code === currencyCode
|
|
85
|
+
);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Calculate crypto amount from USD
|
|
89
|
+
const calculateCryptoAmount = useMemo(() => {
|
|
90
|
+
const amountUsd = form.watch('amount_usd');
|
|
91
|
+
const currencyCode = form.watch('currency_code');
|
|
92
|
+
const pc = getProviderCurrency(currencyCode);
|
|
93
|
+
|
|
94
|
+
if (!pc || !pc.currency.usd_rate || !amountUsd) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const cryptoAmount = amountUsd / pc.currency.usd_rate;
|
|
99
|
+
return {
|
|
100
|
+
amount: cryptoAmount,
|
|
101
|
+
currency: pc.currency.code,
|
|
102
|
+
symbol: pc.currency.symbol || pc.currency.code,
|
|
103
|
+
};
|
|
104
|
+
}, [form.watch('amount_usd'), form.watch('currency_code'), providerCurrencies]);
|
|
105
|
+
|
|
58
106
|
useEventListener(PAYMENTS_DIALOG_EVENTS.OPEN_CREATE_PAYMENT_DIALOG, () => {
|
|
59
107
|
setOpen(true);
|
|
60
108
|
});
|
|
@@ -68,12 +116,25 @@ export const CreatePaymentDialog: React.FC = () => {
|
|
|
68
116
|
form.reset();
|
|
69
117
|
};
|
|
70
118
|
|
|
119
|
+
// Initialize default currency if not set
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (currencyOptions.length > 0 && !form.getValues('currency_code')) {
|
|
122
|
+
form.setValue('currency_code', currencyOptions[0].value as any);
|
|
123
|
+
}
|
|
124
|
+
}, [currencyOptions, form]);
|
|
125
|
+
|
|
71
126
|
const handleSubmit = async (data: PaymentCreateRequest) => {
|
|
72
127
|
try {
|
|
73
128
|
setIsSubmitting(true);
|
|
74
|
-
|
|
129
|
+
|
|
130
|
+
const payment = await createPayment(data);
|
|
75
131
|
handleClose();
|
|
76
132
|
closePaymentsDialog();
|
|
133
|
+
|
|
134
|
+
// Open payment details dialog with created payment
|
|
135
|
+
if (payment) {
|
|
136
|
+
openPaymentDetails(payment);
|
|
137
|
+
}
|
|
77
138
|
} catch (error) {
|
|
78
139
|
paymentsLogger.error('Failed to create payment:', error);
|
|
79
140
|
} finally {
|
|
@@ -81,11 +142,6 @@ export const CreatePaymentDialog: React.FC = () => {
|
|
|
81
142
|
}
|
|
82
143
|
};
|
|
83
144
|
|
|
84
|
-
const currencyOptions = Object.entries(PaymentCreateRequestCurrencyCode).map(([key, value]) => ({
|
|
85
|
-
value,
|
|
86
|
-
label: key,
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
145
|
return (
|
|
90
146
|
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
|
|
91
147
|
<DialogContent className="sm:max-w-md">
|
|
@@ -128,20 +184,51 @@ export const CreatePaymentDialog: React.FC = () => {
|
|
|
128
184
|
render={({ field }) => (
|
|
129
185
|
<FormItem>
|
|
130
186
|
<FormLabel>Currency</FormLabel>
|
|
131
|
-
<
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
187
|
+
<FormControl>
|
|
188
|
+
<Combobox
|
|
189
|
+
options={currencyOptions}
|
|
190
|
+
value={field.value}
|
|
191
|
+
onValueChange={field.onChange}
|
|
192
|
+
placeholder="Select currency..."
|
|
193
|
+
searchPlaceholder="Search currencies..."
|
|
194
|
+
emptyText="No currencies found."
|
|
195
|
+
disabled={isLoadingProviderCurrencies}
|
|
196
|
+
className="w-full"
|
|
197
|
+
renderValue={(option) => {
|
|
198
|
+
if (!option) return null;
|
|
199
|
+
const pc = getProviderCurrency(option.value);
|
|
200
|
+
if (!pc) return option.label;
|
|
201
|
+
return (
|
|
202
|
+
<div className="flex items-center gap-2">
|
|
203
|
+
<TokenIcon symbol={pc.currency.code} size={20} />
|
|
204
|
+
<span>{pc.currency.code}</span>
|
|
205
|
+
{pc.network && (
|
|
206
|
+
<span className="text-xs text-muted-foreground">
|
|
207
|
+
({pc.network.name})
|
|
208
|
+
</span>
|
|
209
|
+
)}
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}}
|
|
213
|
+
renderOption={(option) => {
|
|
214
|
+
const pc = getProviderCurrency(option.value);
|
|
215
|
+
if (!pc) return option.label;
|
|
216
|
+
return (
|
|
217
|
+
<div className="flex items-center gap-2 flex-1">
|
|
218
|
+
<TokenIcon symbol={pc.currency.code} size={24} />
|
|
219
|
+
<div className="flex flex-col flex-1">
|
|
220
|
+
<span className="font-medium">{pc.currency.code} - {pc.currency.name}</span>
|
|
221
|
+
{pc.network && (
|
|
222
|
+
<span className="text-xs text-muted-foreground">
|
|
223
|
+
Network: {pc.network.name} ({pc.network.code})
|
|
224
|
+
</span>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}}
|
|
230
|
+
/>
|
|
231
|
+
</FormControl>
|
|
145
232
|
<FormDescription>
|
|
146
233
|
The cryptocurrency to use for payment.
|
|
147
234
|
</FormDescription>
|
|
@@ -150,37 +237,59 @@ export const CreatePaymentDialog: React.FC = () => {
|
|
|
150
237
|
)}
|
|
151
238
|
/>
|
|
152
239
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
<
|
|
167
|
-
|
|
168
|
-
</
|
|
169
|
-
</
|
|
170
|
-
</
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
240
|
+
{/* Conversion and Fee Information */}
|
|
241
|
+
{calculateCryptoAmount && (() => {
|
|
242
|
+
const pc = getProviderCurrency(form.watch('currency_code'));
|
|
243
|
+
const amountUsd = form.watch('amount_usd');
|
|
244
|
+
if (!pc) return null;
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<div className="rounded-lg bg-muted p-4 space-y-3">
|
|
248
|
+
{/* Amount to Send in Crypto */}
|
|
249
|
+
<div className="flex items-center justify-between">
|
|
250
|
+
<span className="text-sm text-muted-foreground">You will send</span>
|
|
251
|
+
<div className="flex items-center gap-2">
|
|
252
|
+
<TokenIcon symbol={calculateCryptoAmount.currency} size={16} />
|
|
253
|
+
<span className="font-mono font-semibold">
|
|
254
|
+
{calculateCryptoAmount.amount.toFixed(8)} {calculateCryptoAmount.currency}
|
|
255
|
+
</span>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
|
|
259
|
+
{/* USD Amount Received */}
|
|
260
|
+
<div className="flex items-center justify-between">
|
|
261
|
+
<span className="text-sm text-muted-foreground">You will receive</span>
|
|
262
|
+
<span className="text-lg font-bold">
|
|
263
|
+
${amountUsd?.toFixed(2)} USD
|
|
264
|
+
</span>
|
|
265
|
+
</div>
|
|
266
|
+
|
|
267
|
+
{/* Exchange Rate */}
|
|
268
|
+
<div className="flex items-center justify-between text-xs">
|
|
269
|
+
<span className="text-muted-foreground">Rate</span>
|
|
270
|
+
<span className="font-medium">
|
|
271
|
+
1 {calculateCryptoAmount.currency} = ${pc.currency.usd_rate?.toFixed(2)}
|
|
272
|
+
</span>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
{/* Network Info */}
|
|
276
|
+
{pc.network && (
|
|
277
|
+
<div className="border-t pt-3">
|
|
278
|
+
<div className="flex items-center justify-between">
|
|
279
|
+
<span className="text-sm text-muted-foreground">Network</span>
|
|
280
|
+
<span className="text-sm font-medium">{pc.network.name}</span>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
)}
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
})()}
|
|
178
287
|
|
|
179
288
|
<DialogFooter>
|
|
180
289
|
<Button type="button" variant="outline" onClick={handleClose} disabled={isSubmitting}>
|
|
181
290
|
Cancel
|
|
182
291
|
</Button>
|
|
183
|
-
<Button type="submit" disabled={isSubmitting
|
|
292
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
184
293
|
{isSubmitting ? (
|
|
185
294
|
<>
|
|
186
295
|
<RefreshCw className="h-4 w-4 mr-2 animate-spin" />
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Payment Details Dialog
|
|
3
|
+
* Shows payment details with QR code, address, and status
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use client';
|
|
7
|
+
|
|
8
|
+
import React, { useState, useEffect } from 'react';
|
|
9
|
+
import {
|
|
10
|
+
Dialog,
|
|
11
|
+
DialogContent,
|
|
12
|
+
DialogDescription,
|
|
13
|
+
DialogFooter,
|
|
14
|
+
DialogHeader,
|
|
15
|
+
DialogTitle,
|
|
16
|
+
Button,
|
|
17
|
+
TokenIcon,
|
|
18
|
+
useEventListener,
|
|
19
|
+
} from '@djangocfg/ui';
|
|
20
|
+
import { Copy, ExternalLink, CheckCircle2, Clock, XCircle, AlertCircle } from 'lucide-react';
|
|
21
|
+
import type { Payment } from '@djangocfg/api/cfg/contexts';
|
|
22
|
+
|
|
23
|
+
export const PAYMENT_DETAILS_EVENTS = {
|
|
24
|
+
OPEN_PAYMENT_DETAILS: 'open-payment-details',
|
|
25
|
+
CLOSE_PAYMENT_DETAILS: 'close-payment-details',
|
|
26
|
+
} as const;
|
|
27
|
+
|
|
28
|
+
export interface PaymentDetailsDialogProps {
|
|
29
|
+
payment?: Payment | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const PaymentDetailsDialog: React.FC<PaymentDetailsDialogProps> = ({ payment: initialPayment }) => {
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
|
+
const [payment, setPayment] = useState<Payment | null>(initialPayment || null);
|
|
35
|
+
const [copied, setCopied] = useState(false);
|
|
36
|
+
const [timeLeft, setTimeLeft] = useState<string>('');
|
|
37
|
+
|
|
38
|
+
useEventListener(PAYMENT_DETAILS_EVENTS.OPEN_PAYMENT_DETAILS, (event: CustomEvent<{ payment: Payment }>) => {
|
|
39
|
+
setPayment(event.detail.payment);
|
|
40
|
+
setOpen(true);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
useEventListener(PAYMENT_DETAILS_EVENTS.CLOSE_PAYMENT_DETAILS, () => {
|
|
44
|
+
setOpen(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const handleClose = () => {
|
|
48
|
+
setOpen(false);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleCopyAddress = async () => {
|
|
52
|
+
if (payment?.pay_address) {
|
|
53
|
+
await navigator.clipboard.writeText(payment.pay_address);
|
|
54
|
+
setCopied(true);
|
|
55
|
+
setTimeout(() => setCopied(false), 2000);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Calculate time left until expiration
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!payment?.expires_at) return;
|
|
62
|
+
|
|
63
|
+
const updateTimeLeft = () => {
|
|
64
|
+
const now = new Date().getTime();
|
|
65
|
+
const expires = new Date(payment.expires_at!).getTime();
|
|
66
|
+
const diff = expires - now;
|
|
67
|
+
|
|
68
|
+
if (diff <= 0) {
|
|
69
|
+
setTimeLeft('Expired');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const hours = Math.floor(diff / (1000 * 60 * 60));
|
|
74
|
+
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
|
75
|
+
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
|
76
|
+
|
|
77
|
+
setTimeLeft(`${hours}h ${minutes}m ${seconds}s`);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
updateTimeLeft();
|
|
81
|
+
const interval = setInterval(updateTimeLeft, 1000);
|
|
82
|
+
|
|
83
|
+
return () => clearInterval(interval);
|
|
84
|
+
}, [payment?.expires_at]);
|
|
85
|
+
|
|
86
|
+
// Get status icon and color
|
|
87
|
+
const getStatusInfo = () => {
|
|
88
|
+
switch (payment?.status) {
|
|
89
|
+
case 'pending':
|
|
90
|
+
return { icon: Clock, color: 'text-yellow-500', bg: 'bg-yellow-500/10' };
|
|
91
|
+
case 'completed':
|
|
92
|
+
return { icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10' };
|
|
93
|
+
case 'failed':
|
|
94
|
+
return { icon: XCircle, color: 'text-red-500', bg: 'bg-red-500/10' };
|
|
95
|
+
case 'expired':
|
|
96
|
+
return { icon: AlertCircle, color: 'text-gray-500', bg: 'bg-gray-500/10' };
|
|
97
|
+
default:
|
|
98
|
+
return { icon: Clock, color: 'text-gray-500', bg: 'bg-gray-500/10' };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (!payment) return null;
|
|
103
|
+
|
|
104
|
+
const statusInfo = getStatusInfo();
|
|
105
|
+
const StatusIcon = statusInfo.icon;
|
|
106
|
+
|
|
107
|
+
// Generate QR code URL (using simple data URL)
|
|
108
|
+
const qrCodeUrl = payment.pay_address
|
|
109
|
+
? `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(payment.pay_address)}`
|
|
110
|
+
: null;
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
|
|
114
|
+
<DialogContent className="sm:max-w-lg">
|
|
115
|
+
<DialogHeader>
|
|
116
|
+
<DialogTitle>Payment Details</DialogTitle>
|
|
117
|
+
<DialogDescription>
|
|
118
|
+
Send cryptocurrency to complete your payment
|
|
119
|
+
</DialogDescription>
|
|
120
|
+
</DialogHeader>
|
|
121
|
+
|
|
122
|
+
<div className="space-y-6">
|
|
123
|
+
{/* Status Badge */}
|
|
124
|
+
<div className={`flex items-center gap-3 p-4 rounded-lg ${statusInfo.bg}`}>
|
|
125
|
+
<StatusIcon className={`h-5 w-5 ${statusInfo.color}`} />
|
|
126
|
+
<div className="flex-1">
|
|
127
|
+
<div className="font-semibold capitalize">{payment.status}</div>
|
|
128
|
+
{payment.status === 'pending' && timeLeft && (
|
|
129
|
+
<div className="text-sm text-muted-foreground">
|
|
130
|
+
Expires in {timeLeft}
|
|
131
|
+
</div>
|
|
132
|
+
)}
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{/* Amount Information */}
|
|
137
|
+
<div className="space-y-3">
|
|
138
|
+
<div className="flex items-center justify-between p-4 bg-muted rounded-lg">
|
|
139
|
+
<span className="text-sm text-muted-foreground">Amount to send</span>
|
|
140
|
+
<div className="flex items-center gap-2">
|
|
141
|
+
<TokenIcon symbol={payment.currency || 'BTC'} size={20} />
|
|
142
|
+
<span className="font-mono font-bold text-lg">
|
|
143
|
+
{payment.amount_crypto?.toFixed(8)} {payment.currency}
|
|
144
|
+
</span>
|
|
145
|
+
</div>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="flex items-center justify-between px-4">
|
|
149
|
+
<span className="text-sm text-muted-foreground">Equivalent to</span>
|
|
150
|
+
<span className="font-semibold text-lg">${payment.amount_usd?.toFixed(2)} USD</span>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{payment.network && (
|
|
154
|
+
<div className="flex items-center justify-between px-4">
|
|
155
|
+
<span className="text-sm text-muted-foreground">Network</span>
|
|
156
|
+
<span className="font-medium">{payment.network}</span>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* QR Code */}
|
|
162
|
+
{qrCodeUrl && payment.status === 'pending' && (
|
|
163
|
+
<div className="flex justify-center p-6 bg-white rounded-lg">
|
|
164
|
+
<img src={qrCodeUrl} alt="Payment QR Code" className="w-48 h-48" />
|
|
165
|
+
</div>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{/* Payment Address */}
|
|
169
|
+
{payment.pay_address && payment.status === 'pending' && (
|
|
170
|
+
<div className="space-y-2">
|
|
171
|
+
<label className="text-sm font-medium">Payment Address</label>
|
|
172
|
+
<div className="flex items-center gap-2">
|
|
173
|
+
<div className="flex-1 p-3 bg-muted rounded-lg font-mono text-sm break-all">
|
|
174
|
+
{payment.pay_address}
|
|
175
|
+
</div>
|
|
176
|
+
<Button
|
|
177
|
+
variant="outline"
|
|
178
|
+
size="icon"
|
|
179
|
+
onClick={handleCopyAddress}
|
|
180
|
+
className="shrink-0"
|
|
181
|
+
>
|
|
182
|
+
{copied ? (
|
|
183
|
+
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
|
184
|
+
) : (
|
|
185
|
+
<Copy className="h-4 w-4" />
|
|
186
|
+
)}
|
|
187
|
+
</Button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
)}
|
|
191
|
+
|
|
192
|
+
{/* Transaction Hash */}
|
|
193
|
+
{payment.transaction_hash && (
|
|
194
|
+
<div className="space-y-2">
|
|
195
|
+
<label className="text-sm font-medium">Transaction Hash</label>
|
|
196
|
+
<div className="p-3 bg-muted rounded-lg font-mono text-sm break-all">
|
|
197
|
+
{payment.transaction_hash}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Payment URL */}
|
|
203
|
+
{payment.payment_url && payment.status === 'pending' && (
|
|
204
|
+
<Button
|
|
205
|
+
variant="outline"
|
|
206
|
+
className="w-full"
|
|
207
|
+
onClick={() => window.open(payment.payment_url!, '_blank')}
|
|
208
|
+
>
|
|
209
|
+
<ExternalLink className="h-4 w-4 mr-2" />
|
|
210
|
+
Open in Payment Provider
|
|
211
|
+
</Button>
|
|
212
|
+
)}
|
|
213
|
+
|
|
214
|
+
{/* Additional Info */}
|
|
215
|
+
<div className="pt-4 border-t space-y-2 text-xs text-muted-foreground">
|
|
216
|
+
<div className="flex justify-between">
|
|
217
|
+
<span>Payment ID</span>
|
|
218
|
+
<span className="font-mono">{payment.id}</span>
|
|
219
|
+
</div>
|
|
220
|
+
<div className="flex justify-between">
|
|
221
|
+
<span>Created</span>
|
|
222
|
+
<span>{new Date(payment.created_at!).toLocaleString()}</span>
|
|
223
|
+
</div>
|
|
224
|
+
{payment.confirmations_count !== undefined && (
|
|
225
|
+
<div className="flex justify-between">
|
|
226
|
+
<span>Confirmations</span>
|
|
227
|
+
<span>{payment.confirmations_count}</span>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<DialogFooter>
|
|
234
|
+
<Button variant="outline" onClick={handleClose}>
|
|
235
|
+
Close
|
|
236
|
+
</Button>
|
|
237
|
+
</DialogFooter>
|
|
238
|
+
</DialogContent>
|
|
239
|
+
</Dialog>
|
|
240
|
+
);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Helper function to open payment details dialog
|
|
244
|
+
export const openPaymentDetails = (payment: Payment) => {
|
|
245
|
+
window.dispatchEvent(
|
|
246
|
+
new CustomEvent(PAYMENT_DETAILS_EVENTS.OPEN_PAYMENT_DETAILS, {
|
|
247
|
+
detail: { payment },
|
|
248
|
+
})
|
|
249
|
+
);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export const closePaymentDetails = () => {
|
|
253
|
+
window.dispatchEvent(new Event(PAYMENT_DETAILS_EVENTS.CLOSE_PAYMENT_DETAILS));
|
|
254
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { CreateApiKeyDialog } from './CreateApiKeyDialog';
|
|
2
2
|
export { DeleteApiKeyDialog } from './DeleteApiKeyDialog';
|
|
3
3
|
export { CreatePaymentDialog } from './CreatePaymentDialog';
|
|
4
|
+
export { PaymentDetailsDialog, openPaymentDetails, closePaymentDetails } from './PaymentDetailsDialog';
|
|
4
5
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useEffect, type ReactNode } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
api,
|
|
6
|
+
usePaymentsCurrenciesList,
|
|
7
|
+
usePaymentsProviderCurrenciesList,
|
|
8
|
+
usePaymentsNetworksList,
|
|
9
|
+
} from '@djangocfg/api/cfg';
|
|
10
|
+
import type { API } from '@djangocfg/api/cfg';
|
|
11
|
+
import type {
|
|
12
|
+
Currency,
|
|
13
|
+
PaginatedCurrencyListList,
|
|
14
|
+
ProviderCurrency,
|
|
15
|
+
PaginatedProviderCurrencyList,
|
|
16
|
+
Network,
|
|
17
|
+
PaginatedNetworkList,
|
|
18
|
+
} from '@djangocfg/api/cfg';
|
|
19
|
+
|
|
20
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
21
|
+
// Context Type
|
|
22
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export interface RootPaymentsContextValue {
|
|
25
|
+
// Currencies
|
|
26
|
+
currencies: PaginatedCurrencyListList | undefined;
|
|
27
|
+
isLoadingCurrencies: boolean;
|
|
28
|
+
currenciesError: Error | undefined;
|
|
29
|
+
refreshCurrencies: () => Promise<void>;
|
|
30
|
+
|
|
31
|
+
// Provider Currencies
|
|
32
|
+
providerCurrencies: PaginatedProviderCurrencyList | undefined;
|
|
33
|
+
isLoadingProviderCurrencies: boolean;
|
|
34
|
+
providerCurrenciesError: Error | undefined;
|
|
35
|
+
refreshProviderCurrencies: () => Promise<void>;
|
|
36
|
+
|
|
37
|
+
// Networks
|
|
38
|
+
networks: PaginatedNetworkList | undefined;
|
|
39
|
+
isLoadingNetworks: boolean;
|
|
40
|
+
networksError: Error | undefined;
|
|
41
|
+
refreshNetworks: () => Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
45
|
+
// Context
|
|
46
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
const RootPaymentsContext = createContext<RootPaymentsContextValue | undefined>(undefined);
|
|
49
|
+
|
|
50
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
51
|
+
// Provider
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
export function RootPaymentsProvider({ children }: { children: ReactNode }) {
|
|
55
|
+
// List all currencies
|
|
56
|
+
const {
|
|
57
|
+
data: currencies,
|
|
58
|
+
error: currenciesError,
|
|
59
|
+
isLoading: isLoadingCurrencies,
|
|
60
|
+
mutate: mutateCurrencies,
|
|
61
|
+
} = usePaymentsCurrenciesList({}, api as unknown as API);
|
|
62
|
+
|
|
63
|
+
// List all provider currencies
|
|
64
|
+
const {
|
|
65
|
+
data: providerCurrencies,
|
|
66
|
+
error: providerCurrenciesError,
|
|
67
|
+
isLoading: isLoadingProviderCurrencies,
|
|
68
|
+
mutate: mutateProviderCurrencies,
|
|
69
|
+
} = usePaymentsProviderCurrenciesList({}, api as unknown as API);
|
|
70
|
+
|
|
71
|
+
// List all networks
|
|
72
|
+
const {
|
|
73
|
+
data: networks,
|
|
74
|
+
error: networksError,
|
|
75
|
+
isLoading: isLoadingNetworks,
|
|
76
|
+
mutate: mutateNetworks,
|
|
77
|
+
} = usePaymentsNetworksList({}, api as unknown as API);
|
|
78
|
+
|
|
79
|
+
const refreshCurrencies = async () => {
|
|
80
|
+
await mutateCurrencies();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const refreshProviderCurrencies = async () => {
|
|
84
|
+
await mutateProviderCurrencies();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const refreshNetworks = async () => {
|
|
88
|
+
await mutateNetworks();
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const value: RootPaymentsContextValue = {
|
|
92
|
+
currencies,
|
|
93
|
+
isLoadingCurrencies,
|
|
94
|
+
currenciesError,
|
|
95
|
+
refreshCurrencies,
|
|
96
|
+
providerCurrencies,
|
|
97
|
+
isLoadingProviderCurrencies,
|
|
98
|
+
providerCurrenciesError,
|
|
99
|
+
refreshProviderCurrencies,
|
|
100
|
+
networks,
|
|
101
|
+
isLoadingNetworks,
|
|
102
|
+
networksError,
|
|
103
|
+
refreshNetworks,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<RootPaymentsContext.Provider value={value}>{children}</RootPaymentsContext.Provider>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// Hook
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
export function useRootPaymentsContext(): RootPaymentsContextValue {
|
|
116
|
+
const context = useContext(RootPaymentsContext);
|
|
117
|
+
if (!context) {
|
|
118
|
+
throw new Error('useRootPaymentsContext must be used within RootPaymentsProvider');
|
|
119
|
+
}
|
|
120
|
+
return context;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
124
|
+
// Re-export types
|
|
125
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export type {
|
|
128
|
+
Currency,
|
|
129
|
+
PaginatedCurrencyListList,
|
|
130
|
+
ProviderCurrency,
|
|
131
|
+
PaginatedProviderCurrencyList,
|
|
132
|
+
Network,
|
|
133
|
+
PaginatedNetworkList,
|
|
134
|
+
};
|
|
@@ -16,15 +16,15 @@ import {
|
|
|
16
16
|
Skeleton,
|
|
17
17
|
} from '@djangocfg/ui';
|
|
18
18
|
import { Wallet, RefreshCw, Plus } from 'lucide-react';
|
|
19
|
-
import {
|
|
19
|
+
import { useOverviewContext } from '@djangocfg/api/cfg/contexts';
|
|
20
20
|
import { openCreatePaymentDialog } from '../../../events';
|
|
21
21
|
|
|
22
22
|
export const BalanceCard: React.FC = () => {
|
|
23
|
-
const {
|
|
24
|
-
balanceSummary,
|
|
25
|
-
isLoadingSummary,
|
|
26
|
-
refreshSummary
|
|
27
|
-
} =
|
|
23
|
+
const {
|
|
24
|
+
balanceSummary,
|
|
25
|
+
isLoadingSummary,
|
|
26
|
+
refreshSummary
|
|
27
|
+
} = useOverviewContext();
|
|
28
28
|
|
|
29
29
|
const formatCurrency = (amount?: number | null) => {
|
|
30
30
|
if (amount === null || amount === undefined) return '$0.00';
|
|
@@ -29,7 +29,8 @@ import {
|
|
|
29
29
|
} from '@djangocfg/ui';
|
|
30
30
|
import { Plus, Search, Filter, ChevronLeft, ChevronRight, RefreshCw, ExternalLink } from 'lucide-react';
|
|
31
31
|
import { usePaymentsContext } from '@djangocfg/api/cfg/contexts';
|
|
32
|
-
import { openCreatePaymentDialog
|
|
32
|
+
import { openCreatePaymentDialog } from '../../../events';
|
|
33
|
+
import { openPaymentDetails } from '../../../components/PaymentDetailsDialog';
|
|
33
34
|
|
|
34
35
|
export const PaymentsList: React.FC = () => {
|
|
35
36
|
const {
|
|
@@ -197,7 +198,11 @@ export const PaymentsList: React.FC = () => {
|
|
|
197
198
|
</TableHeader>
|
|
198
199
|
<TableBody>
|
|
199
200
|
{paymentsList.map((payment) => (
|
|
200
|
-
<TableRow
|
|
201
|
+
<TableRow
|
|
202
|
+
key={payment.id}
|
|
203
|
+
className="cursor-pointer hover:bg-accent"
|
|
204
|
+
onClick={() => openPaymentDetails(payment)}
|
|
205
|
+
>
|
|
201
206
|
<TableCell>
|
|
202
207
|
<div>
|
|
203
208
|
<div className="font-medium">
|
|
@@ -229,7 +234,7 @@ export const PaymentsList: React.FC = () => {
|
|
|
229
234
|
size="sm"
|
|
230
235
|
onClick={(e) => {
|
|
231
236
|
e.stopPropagation();
|
|
232
|
-
|
|
237
|
+
openPaymentDetails(payment);
|
|
233
238
|
}}
|
|
234
239
|
>
|
|
235
240
|
<ExternalLink className="h-4 w-4" />
|