@djangocfg/layouts 1.0.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/layouts",
3
- "version": "1.0.1",
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.1",
58
- "@djangocfg/og-image": "^1.0.1",
59
- "@djangocfg/ui": "^1.0.1",
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.1",
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
- {getChannelIcon()}
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
- {getChannelIcon()}
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 ? 'Verifying...' : 'Verify Code'}
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
- Back
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
- Resend
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, { useState } from '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 Content
25
+ // Payments Layout
27
26
  // ─────────────────────────────────────────────────────────────────────────
28
27
 
29
- const PaymentsLayoutContent: React.FC = () => {
30
- const [activeTab, setActiveTab] = useState<PaymentTab>('overview');
28
+ export interface PaymentsLayoutProps {
29
+ children?: React.ReactNode;
30
+ }
31
31
 
32
+ export const PaymentsLayout: React.FC<PaymentsLayoutProps> = () => {
32
33
  return (
33
- <div className="h-full p-6 space-y-6">
34
- {/* Page Header */}
35
- <div>
36
- <h1 className="text-3xl font-bold tracking-tight">Payments & Billing</h1>
37
- <p className="text-muted-foreground">
38
- Manage your payments, subscriptions, API keys, and account balance
39
- </p>
40
- </div>
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
- {/* Overview Tab */}
69
+ {/* Each tab wrapped in its own provider - loads only when tab is active */}
72
70
  <TabsContent value="overview" className="space-y-6">
73
- <OverviewView />
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
- <PaymentsView />
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
- <ApiKeysView />
91
+ <ApiKeysProvider>
92
+ <ApiKeysView />
93
+ <CreateApiKeyDialog />
94
+ <DeleteApiKeyDialog />
95
+ </ApiKeysProvider>
89
96
  </TabsContent>
90
97
 
91
- {/* Tariffs Tab */}
92
- <TabsContent value="tariffs" className="space-y-6">
93
- <TariffsView />
94
- </TabsContent>
95
- </Tabs>
98
+ <TabsContent value="tariffs" className="space-y-6">
99
+ <TariffsView />
100
+ </TabsContent>
101
+ </Tabs>
96
102
 
97
- {/* Global Dialogs */}
98
- <CreateApiKeyDialog />
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
- Select,
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
- await createPayment(data);
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
- <Select value={field.value} onValueChange={field.onChange}>
132
- <FormControl>
133
- <SelectTrigger>
134
- <SelectValue placeholder="Select currency" />
135
- </SelectTrigger>
136
- </FormControl>
137
- <SelectContent>
138
- {currencyOptions.map((option) => (
139
- <SelectItem key={option.value} value={option.value}>
140
- {option.label}
141
- </SelectItem>
142
- ))}
143
- </SelectContent>
144
- </Select>
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
- <FormField
154
- control={form.control}
155
- name="provider"
156
- render={({ field }) => (
157
- <FormItem>
158
- <FormLabel>Payment Provider</FormLabel>
159
- <Select value={field.value} onValueChange={field.onChange}>
160
- <FormControl>
161
- <SelectTrigger>
162
- <SelectValue placeholder="Select provider" />
163
- </SelectTrigger>
164
- </FormControl>
165
- <SelectContent>
166
- <SelectItem value={PaymentCreateRequestProvider.NOWPAYMENTS}>
167
- NOWPayments
168
- </SelectItem>
169
- </SelectContent>
170
- </Select>
171
- <FormDescription>
172
- The payment gateway to process your payment.
173
- </FormDescription>
174
- <FormMessage />
175
- </FormItem>
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 || !form.formState.isValid}>
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 { useBalancesContext } from '@djangocfg/api/cfg/contexts';
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
- } = useBalancesContext();
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, openPaymentDetailsDialog } from '../../../events';
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 key={payment.id} className="cursor-pointer hover:bg-accent">
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
- openPaymentDetailsDialog(payment.id);
237
+ openPaymentDetails(payment);
233
238
  }}
234
239
  >
235
240
  <ExternalLink className="h-4 w-4" />
@@ -11,7 +11,9 @@ import { MessageSquare, Loader2, User, Headphones } from 'lucide-react';
11
11
  import { useSupportLayoutContext } from '../context';
12
12
  import { useInfiniteMessages } from '../hooks';
13
13
  import { useAuth } from '../../../auth';
14
- import type { Message } from '@djangocfg/api/cfg/generated/schemas';
14
+ import { CfgSupportTypes } from '@djangocfg/api';
15
+
16
+ type Message = CfgSupportTypes.Message;
15
17
 
16
18
  const formatTime = (date: string | null | undefined): string => {
17
19
  if (!date) return '';
@@ -7,8 +7,11 @@
7
7
 
8
8
  import React, { createContext, useCallback, useContext, useEffect, useState, type ReactNode } from 'react';
9
9
  import { useSupportContext, type Ticket } from '@djangocfg/api/cfg/contexts';
10
- import type { Message, MessageCreateRequest } from '@djangocfg/api/cfg/generated/schemas';
10
+ import { CfgSupportTypes } from '@djangocfg/api';
11
11
  import { supportLogger } from '../../../utils/logger';
12
+
13
+ type Message = CfgSupportTypes.Message;
14
+ type MessageCreateRequest = CfgSupportTypes.MessageCreateRequest;
12
15
  import { useAuth } from '../../../auth';
13
16
  import { SUPPORT_LAYOUT_EVENTS } from '../events';
14
17
  import { useInfiniteMessages } from '../hooks';
@@ -4,10 +4,12 @@
4
4
  */
5
5
 
6
6
  import useSWRInfinite from 'swr/infinite';
7
- import { api } from '@djangocfg/api';
8
- import type { API } from '@djangocfg/api/cfg/generated';
9
- import { getSupportTicketsMessagesList } from '@djangocfg/api/cfg/generated/fetchers';
10
- import type { PaginatedMessageList, Message } from '@djangocfg/api/cfg/generated/schemas';
7
+ import { api, getSupportTicketsMessagesList } from '@djangocfg/api';
8
+ import type { API } from '@djangocfg/api';
9
+ import { CfgSupportTypes } from '@djangocfg/api';
10
+
11
+ type PaginatedMessageList = CfgSupportTypes.PaginatedMessageList;
12
+ type Message = CfgSupportTypes.Message;
11
13
 
12
14
  const PAGE_SIZE = 20;
13
15
 
@@ -4,10 +4,12 @@
4
4
  */
5
5
 
6
6
  import useSWRInfinite from 'swr/infinite';
7
- import { api } from '@djangocfg/api';
8
- import type { API } from '@djangocfg/api/cfg/generated';
9
- import { getSupportTicketsList } from '@djangocfg/api/cfg/generated/fetchers';
10
- import type { PaginatedTicketList, Ticket } from '@djangocfg/api/cfg/generated/schemas';
7
+ import { api, getSupportTicketsList } from '@djangocfg/api';
8
+ import type { API } from '@djangocfg/api';
9
+ import { CfgSupportTypes } from '@djangocfg/api';
10
+
11
+ type PaginatedTicketList = CfgSupportTypes.PaginatedTicketList;
12
+ type Ticket = CfgSupportTypes.Ticket;
11
13
 
12
14
  const PAGE_SIZE = 20;
13
15
 
@@ -4,9 +4,8 @@
4
4
  */
5
5
 
6
6
  import useSWRInfinite from 'swr/infinite';
7
- import { api } from '@djangocfg/api';
8
- import type { API, CfgKnowbaseTypes } from '@djangocfg/api/cfg/generated';
9
- import { getKnowbaseAdminSessionsList } from '@djangocfg/api/cfg/generated/fetchers';
7
+ import { api, getKnowbaseAdminSessionsList, CfgKnowbaseTypes } from '@djangocfg/api';
8
+ import type { API } from '@djangocfg/api';
10
9
 
11
10
  type PaginatedChatSessionList = CfgKnowbaseTypes.PaginatedChatSessionList;
12
11
  type ChatSession = CfgKnowbaseTypes.ChatSession;