@djangocfg/layouts 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/package.json +5 -5
  2. package/src/layouts/AppLayout/layouts/AuthLayout/AuthHelp.tsx +7 -5
  3. package/src/layouts/AppLayout/layouts/AuthLayout/IdentifierForm.tsx +3 -3
  4. package/src/layouts/AppLayout/layouts/AuthLayout/OTPForm.tsx +26 -10
  5. package/src/layouts/AppLayout/layouts/PrivateLayout/components/DashboardSidebar.tsx +1 -1
  6. package/src/layouts/AppLayout/layouts/PublicLayout/components/DesktopUserMenu.tsx +6 -6
  7. package/src/layouts/AppLayout/layouts/PublicLayout/components/Footer.tsx +1 -1
  8. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenu.tsx +43 -133
  9. package/src/layouts/AppLayout/layouts/PublicLayout/components/MobileMenuUserCard.tsx +150 -0
  10. package/src/layouts/AppLayout/layouts/PublicLayout/components/Navigation.tsx +2 -2
  11. package/src/layouts/PaymentsLayout/PaymentsLayout.tsx +41 -57
  12. package/src/layouts/PaymentsLayout/components/CreatePaymentDialog.tsx +188 -57
  13. package/src/layouts/PaymentsLayout/components/PaymentDetailsDialog.tsx +323 -0
  14. package/src/layouts/PaymentsLayout/components/index.ts +1 -0
  15. package/src/layouts/PaymentsLayout/context/RootPaymentsContext.tsx +129 -0
  16. package/src/layouts/PaymentsLayout/views/apikeys/components/ApiKeysList.tsx +2 -2
  17. package/src/layouts/PaymentsLayout/views/overview/components/BalanceCard.tsx +6 -6
  18. package/src/layouts/PaymentsLayout/views/overview/components/RecentPayments.tsx +2 -2
  19. package/src/layouts/PaymentsLayout/views/payments/components/PaymentsList.tsx +9 -4
  20. package/src/layouts/SupportLayout/hooks/useInfiniteMessages.ts +2 -3
  21. package/src/layouts/SupportLayout/hooks/useInfiniteTickets.ts +2 -3
  22. package/src/snippets/Chat/components/SessionList.tsx +1 -1
  23. package/src/snippets/Chat/hooks/useInfiniteSessions.ts +2 -2
  24. package/src/snippets/VideoPlayer/VideoPlayer.tsx +1 -1
@@ -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,40 +21,108 @@ 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';
36
- import { Schemas, Enums } from '@djangocfg/api/cfg/generated';
32
+ import { usePaymentsContext, useRootPaymentsContext } from '@djangocfg/api/cfg/contexts';
33
+ import { Schemas } 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;
42
- const { PaymentCreateRequestCurrencyCode, PaymentCreateRequestProvider } = Enums;
43
42
 
44
43
  export const CreatePaymentDialog: React.FC = () => {
45
44
  const [open, setOpen] = useState(false);
46
45
  const [isSubmitting, setIsSubmitting] = useState(false);
46
+
47
47
  const { createPayment } = usePaymentsContext();
48
+ const {
49
+ providerCurrencies,
50
+ isLoadingProviderCurrencies,
51
+ } = useRootPaymentsContext();
48
52
 
49
53
  const form = useForm<PaymentCreateRequest>({
50
54
  resolver: zodResolver(PaymentCreateRequestSchema),
51
55
  defaultValues: {
52
56
  amount_usd: 10,
53
- currency_code: PaymentCreateRequestCurrencyCode.USDT,
54
- provider: PaymentCreateRequestProvider.NOWPAYMENTS,
57
+ currency_code: 'USDT', // Default to USDT
55
58
  },
56
59
  });
57
60
 
61
+ // Group currencies by token and create combobox options
62
+ // Backend automatically selects network, so we group by currency code
63
+ const currencyOptions = useMemo((): ComboboxOption[] => {
64
+ if (!providerCurrencies?.results) return [];
65
+
66
+ const enabledCurrencies = providerCurrencies.results.filter(pc => pc.is_enabled);
67
+
68
+ // Group by currency code and collect all networks
69
+ const currencyMap = new Map<string, { pc: ProviderCurrency, networks: string[] }>();
70
+
71
+ for (const pc of enabledCurrencies) {
72
+ const code = pc.currency.code.toUpperCase();
73
+ if (!currencyMap.has(code)) {
74
+ currencyMap.set(code, { pc, networks: [] });
75
+ }
76
+ if (pc.network?.name) {
77
+ currencyMap.get(code)!.networks.push(pc.network.name);
78
+ }
79
+ }
80
+
81
+ return Array.from(currencyMap.values()).map(({ pc, networks }) => {
82
+ // Show network info only if there's exactly one network, otherwise backend will choose
83
+ let label = pc.currency.code;
84
+ let description = pc.currency.name;
85
+
86
+ if (networks.length === 1) {
87
+ label = `${pc.currency.code} (${networks[0]})`;
88
+ } else if (networks.length > 1) {
89
+ description = `${pc.currency.name} • Multiple networks available`;
90
+ }
91
+
92
+ return {
93
+ // Use currency code (e.g. "USDT") as value for API
94
+ value: pc.currency.code.toUpperCase(),
95
+ label,
96
+ description,
97
+ };
98
+ });
99
+ }, [providerCurrencies]);
100
+
101
+ // Get first ProviderCurrency by currency code (e.g. "USDT")
102
+ const getProviderCurrency = (currencyCode: string) => {
103
+ return providerCurrencies?.results?.find(
104
+ pc => pc.is_enabled && pc.currency.code.toUpperCase() === currencyCode.toUpperCase()
105
+ );
106
+ };
107
+
108
+ // Calculate crypto amount from USD
109
+ const calculateCryptoAmount = useMemo(() => {
110
+ const amountUsd = form.watch('amount_usd');
111
+ const currencyCode = form.watch('currency_code');
112
+ const pc = getProviderCurrency(currencyCode);
113
+
114
+ if (!pc || !pc.currency.usd_rate || !amountUsd) {
115
+ return null;
116
+ }
117
+
118
+ const cryptoAmount = amountUsd / pc.currency.usd_rate;
119
+ return {
120
+ amount: cryptoAmount,
121
+ currency: pc.currency.code,
122
+ symbol: pc.currency.symbol || pc.currency.code,
123
+ };
124
+ }, [form.watch('amount_usd'), form.watch('currency_code'), providerCurrencies]);
125
+
58
126
  useEventListener(PAYMENTS_DIALOG_EVENTS.OPEN_CREATE_PAYMENT_DIALOG, () => {
59
127
  setOpen(true);
60
128
  });
@@ -68,12 +136,29 @@ export const CreatePaymentDialog: React.FC = () => {
68
136
  form.reset();
69
137
  };
70
138
 
139
+ // Initialize default currency if not set
140
+ useEffect(() => {
141
+ if (currencyOptions.length > 0 && !form.getValues('currency_code')) {
142
+ form.setValue('currency_code', currencyOptions[0].value as any);
143
+ }
144
+ }, [currencyOptions, form]);
145
+
71
146
  const handleSubmit = async (data: PaymentCreateRequest) => {
72
147
  try {
73
148
  setIsSubmitting(true);
74
- await createPayment(data);
149
+
150
+ const result = await createPayment(data);
75
151
  handleClose();
76
152
  closePaymentsDialog();
153
+
154
+ // The API returns a wrapped response with { success, message, payment }
155
+ // Extract the payment ID from the result
156
+ const paymentData = result as any;
157
+ const paymentId = paymentData?.payment?.id || paymentData?.id;
158
+
159
+ if (paymentId) {
160
+ openPaymentDetails(String(paymentId));
161
+ }
77
162
  } catch (error) {
78
163
  paymentsLogger.error('Failed to create payment:', error);
79
164
  } finally {
@@ -81,11 +166,6 @@ export const CreatePaymentDialog: React.FC = () => {
81
166
  }
82
167
  };
83
168
 
84
- const currencyOptions = Object.entries(PaymentCreateRequestCurrencyCode).map(([key, value]) => ({
85
- value,
86
- label: key,
87
- }));
88
-
89
169
  return (
90
170
  <Dialog open={open} onOpenChange={(isOpen) => !isOpen && handleClose()}>
91
171
  <DialogContent className="sm:max-w-md">
@@ -128,20 +208,49 @@ export const CreatePaymentDialog: React.FC = () => {
128
208
  render={({ field }) => (
129
209
  <FormItem>
130
210
  <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>
211
+ <FormControl>
212
+ <Combobox
213
+ options={currencyOptions}
214
+ value={field.value}
215
+ onValueChange={field.onChange}
216
+ placeholder="Select currency..."
217
+ searchPlaceholder="Search currencies..."
218
+ emptyText="No currencies found."
219
+ disabled={isLoadingProviderCurrencies}
220
+ className="w-full"
221
+ renderValue={(option) => {
222
+ if (!option) return null;
223
+ const pc = getProviderCurrency(option.value);
224
+ if (!pc) return option.label;
225
+ return (
226
+ <div className="flex items-center gap-2">
227
+ <TokenIcon symbol={pc.currency.code} size={20} />
228
+ <span>{pc.currency.code}</span>
229
+ {pc.network && (
230
+ <span className="text-xs text-muted-foreground">
231
+ ({pc.network.name})
232
+ </span>
233
+ )}
234
+ </div>
235
+ );
236
+ }}
237
+ renderOption={(option) => {
238
+ const pc = getProviderCurrency(option.value);
239
+ if (!pc) return option.label;
240
+ return (
241
+ <div className="flex items-center gap-2 flex-1 min-w-0">
242
+ <TokenIcon symbol={pc.currency.code} size={20} className="shrink-0" />
243
+ <div className="flex flex-col flex-1 min-w-0">
244
+ <span className="font-medium truncate">
245
+ {pc.currency.code}
246
+ {pc.network && ` (${pc.network.name})`}
247
+ </span>
248
+ </div>
249
+ </div>
250
+ );
251
+ }}
252
+ />
253
+ </FormControl>
145
254
  <FormDescription>
146
255
  The cryptocurrency to use for payment.
147
256
  </FormDescription>
@@ -150,37 +259,59 @@ export const CreatePaymentDialog: React.FC = () => {
150
259
  )}
151
260
  />
152
261
 
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
- />
262
+ {/* Conversion and Fee Information */}
263
+ {calculateCryptoAmount && (() => {
264
+ const pc = getProviderCurrency(form.watch('currency_code'));
265
+ const amountUsd = form.watch('amount_usd');
266
+ if (!pc) return null;
267
+
268
+ return (
269
+ <div className="rounded-sm bg-muted p-4 space-y-3">
270
+ {/* Amount to Send in Crypto */}
271
+ <div className="flex items-center justify-between">
272
+ <span className="text-sm text-muted-foreground">You will send</span>
273
+ <div className="flex items-center gap-2">
274
+ <TokenIcon symbol={calculateCryptoAmount.currency} size={16} />
275
+ <span className="font-mono font-semibold">
276
+ {calculateCryptoAmount.amount.toFixed(8)} {calculateCryptoAmount.currency}
277
+ </span>
278
+ </div>
279
+ </div>
280
+
281
+ {/* USD Amount Received */}
282
+ <div className="flex items-center justify-between">
283
+ <span className="text-sm text-muted-foreground">You will receive</span>
284
+ <span className="text-lg font-bold">
285
+ ${amountUsd?.toFixed(2)} USD
286
+ </span>
287
+ </div>
288
+
289
+ {/* Exchange Rate */}
290
+ <div className="flex items-center justify-between text-xs">
291
+ <span className="text-muted-foreground">Rate</span>
292
+ <span className="font-medium">
293
+ 1 {calculateCryptoAmount.currency} = ${pc.currency.usd_rate?.toFixed(2)}
294
+ </span>
295
+ </div>
296
+
297
+ {/* Network Info */}
298
+ {pc.network && (
299
+ <div className="border-t pt-3">
300
+ <div className="flex items-center justify-between">
301
+ <span className="text-sm text-muted-foreground">Network</span>
302
+ <span className="text-sm font-medium">{pc.network.name}</span>
303
+ </div>
304
+ </div>
305
+ )}
306
+ </div>
307
+ );
308
+ })()}
178
309
 
179
310
  <DialogFooter>
180
311
  <Button type="button" variant="outline" onClick={handleClose} disabled={isSubmitting}>
181
312
  Cancel
182
313
  </Button>
183
- <Button type="submit" disabled={isSubmitting || !form.formState.isValid}>
314
+ <Button type="submit" disabled={isSubmitting}>
184
315
  {isSubmitting ? (
185
316
  <>
186
317
  <RefreshCw className="h-4 w-4 mr-2 animate-spin" />