feat: Complete licensing module with admin and customer UIs

Admin SPA:
- Licenses list page with search, filter, pagination
- License detail page with activation history
- Copy license key, view details, revoke functionality

Customer SPA:
- My Account > Licenses page
- View licenses with activation info
- Copy license key
- Deactivate devices

Backend integration:
- Routes registered in App.tsx and Account/index.tsx
- License nav item in account sidebar (conditional on module enabled)
This commit is contained in:
Dwindi Ramadhana
2026-01-05 16:29:37 +07:00
parent b367c1fcf8
commit 3d2bab90ec
6 changed files with 798 additions and 5 deletions

View File

@@ -19,6 +19,8 @@ import ProductEdit from '@/routes/Products/Edit';
import ProductCategories from '@/routes/Products/Categories'; import ProductCategories from '@/routes/Products/Categories';
import ProductTags from '@/routes/Products/Tags'; import ProductTags from '@/routes/Products/Tags';
import ProductAttributes from '@/routes/Products/Attributes'; import ProductAttributes from '@/routes/Products/Attributes';
import Licenses from '@/routes/Products/Licenses';
import LicenseDetail from '@/routes/Products/Licenses/Detail';
import CouponsIndex from '@/routes/Marketing/Coupons'; import CouponsIndex from '@/routes/Marketing/Coupons';
import CouponNew from '@/routes/Marketing/Coupons/New'; import CouponNew from '@/routes/Marketing/Coupons/New';
import CouponEdit from '@/routes/Marketing/Coupons/Edit'; import CouponEdit from '@/routes/Marketing/Coupons/Edit';
@@ -541,6 +543,8 @@ function AppRoutes() {
<Route path="/products/categories" element={<ProductCategories />} /> <Route path="/products/categories" element={<ProductCategories />} />
<Route path="/products/tags" element={<ProductTags />} /> <Route path="/products/tags" element={<ProductTags />} />
<Route path="/products/attributes" element={<ProductAttributes />} /> <Route path="/products/attributes" element={<ProductAttributes />} />
<Route path="/products/licenses" element={<Licenses />} />
<Route path="/products/licenses/:id" element={<LicenseDetail />} />
{/* Orders */} {/* Orders */}
<Route path="/orders" element={<OrdersIndex />} /> <Route path="/orders" element={<OrdersIndex />} />

View File

@@ -0,0 +1,233 @@
import React from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import { ArrowLeft, Key, Monitor, Globe, Clock } from 'lucide-react';
import { __ } from '@/lib/i18n';
interface Activation {
id: number;
license_id: number;
domain: string | null;
ip_address: string | null;
machine_id: string | null;
user_agent: string | null;
status: 'active' | 'deactivated';
activated_at: string;
deactivated_at: string | null;
}
interface LicenseDetail {
id: number;
license_key: string;
product_id: number;
product_name: string;
order_id: number;
user_id: number;
user_email: string;
user_name: string;
status: 'active' | 'revoked' | 'expired';
activation_limit: number;
activation_count: number;
activations_remaining: number;
expires_at: string | null;
is_expired: boolean;
created_at: string;
updated_at: string;
activations: Activation[];
}
export default function LicenseDetail() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { data: license, isLoading } = useQuery<LicenseDetail>({
queryKey: ['license', id],
queryFn: () => api.get(`/licenses/${id}`),
enabled: !!id,
});
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
</div>
);
}
if (!license) {
return (
<div className="text-center py-12">
<p className="text-muted-foreground">{__('License not found')}</p>
<Button variant="link" onClick={() => navigate('/products/licenses')}>
{__('Back to Licenses')}
</Button>
</div>
);
}
const getStatusBadge = () => {
if (license.status === 'revoked') {
return <Badge variant="destructive">{__('Revoked')}</Badge>;
}
if (license.is_expired) {
return <Badge variant="secondary">{__('Expired')}</Badge>;
}
return <Badge variant="default">{__('Active')}</Badge>;
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-4">
<Button variant="ghost" size="icon" onClick={() => navigate('/products/licenses')}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Key className="h-6 w-6" />
{__('License Details')}
</h1>
<p className="text-muted-foreground font-mono text-sm">{license.license_key}</p>
</div>
<div className="ml-auto">{getStatusBadge()}</div>
</div>
{/* Info Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{__('Product')}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-semibold">{license.product_name}</p>
<p className="text-xs text-muted-foreground">Order #{license.order_id}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{__('Customer')}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-semibold">{license.user_name}</p>
<p className="text-xs text-muted-foreground">{license.user_email}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium">{__('Activations')}</CardTitle>
</CardHeader>
<CardContent>
<p className="font-semibold">
{license.activation_count} / {license.activation_limit === 0 ? '∞' : license.activation_limit}
</p>
<p className="text-xs text-muted-foreground">
{license.activations_remaining === -1 ? __('Unlimited') : `${license.activations_remaining} ${__('remaining')}`}
</p>
</CardContent>
</Card>
</div>
{/* Dates */}
<Card>
<CardHeader>
<CardTitle className="text-sm font-medium">{__('Dates')}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<p className="text-muted-foreground">{__('Created')}</p>
<p>{new Date(license.created_at).toLocaleString()}</p>
</div>
<div>
<p className="text-muted-foreground">{__('Expires')}</p>
<p className={license.is_expired ? 'text-red-500' : ''}>
{license.expires_at ? new Date(license.expires_at).toLocaleDateString() : __('Never')}
</p>
</div>
</div>
</CardContent>
</Card>
{/* Activations */}
<Card>
<CardHeader>
<CardTitle>{__('Activation History')}</CardTitle>
<CardDescription>
{__('All activations and deactivations for this license')}
</CardDescription>
</CardHeader>
<CardContent>
{license.activations.length === 0 ? (
<p className="text-center py-8 text-muted-foreground">
{__('No activations yet')}
</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>{__('Domain/Machine')}</TableHead>
<TableHead>{__('IP Address')}</TableHead>
<TableHead>{__('Activated')}</TableHead>
<TableHead>{__('Status')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{license.activations.map((activation) => (
<TableRow key={activation.id}>
<TableCell>
<div className="flex items-center gap-2">
{activation.domain ? (
<>
<Globe className="h-4 w-4 text-muted-foreground" />
<span>{activation.domain}</span>
</>
) : activation.machine_id ? (
<>
<Monitor className="h-4 w-4 text-muted-foreground" />
<span className="font-mono text-xs">{activation.machine_id}</span>
</>
) : (
<span className="text-muted-foreground">{__('Unknown')}</span>
)}
</div>
</TableCell>
<TableCell>
<span className="font-mono text-xs">{activation.ip_address || '-'}</span>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Clock className="h-3 w-3 text-muted-foreground" />
{new Date(activation.activated_at).toLocaleString()}
</div>
</TableCell>
<TableCell>
{activation.status === 'active' ? (
<Badge variant="default">{__('Active')}</Badge>
) : (
<Badge variant="secondary">{__('Deactivated')}</Badge>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</div>
);
}

View File

@@ -0,0 +1,292 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Search, Key, Ban, Eye, Copy, Check } from 'lucide-react';
import { toast } from 'sonner';
import { __ } from '@/lib/i18n';
import { useNavigate } from 'react-router-dom';
interface License {
id: number;
license_key: string;
product_id: number;
product_name: string;
order_id: number;
user_id: number;
user_email: string;
user_name: string;
status: 'active' | 'revoked' | 'expired';
activation_limit: number;
activation_count: number;
activations_remaining: number;
expires_at: string | null;
is_expired: boolean;
created_at: string;
}
interface LicensesResponse {
licenses: License[];
total: number;
page: number;
per_page: number;
}
export default function Licenses() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [search, setSearch] = useState('');
const [status, setStatus] = useState<string>('');
const [page, setPage] = useState(1);
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const { data, isLoading } = useQuery<LicensesResponse>({
queryKey: ['licenses', { search, status, page }],
queryFn: () => api.get('/licenses', {
params: { search, status: status || undefined, page, per_page: 20 }
}),
});
const revokeMutation = useMutation({
mutationFn: (id: number) => api.del(`/licenses/${id}`),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['licenses'] });
toast.success(__('License revoked'));
},
onError: () => {
toast.error(__('Failed to revoke license'));
},
});
const copyToClipboard = (key: string) => {
navigator.clipboard.writeText(key);
setCopiedKey(key);
setTimeout(() => setCopiedKey(null), 2000);
toast.success(__('License key copied'));
};
const getStatusBadge = (license: License) => {
if (license.status === 'revoked') {
return <Badge variant="destructive">{__('Revoked')}</Badge>;
}
if (license.is_expired) {
return <Badge variant="secondary">{__('Expired')}</Badge>;
}
return <Badge variant="default">{__('Active')}</Badge>;
};
const totalPages = data ? Math.ceil(data.total / data.per_page) : 1;
return (
<div className="space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Key className="h-6 w-6" />
{__('Licenses')}
</h1>
<p className="text-muted-foreground">
{__('Manage software licenses for your digital products')}
</p>
</div>
{/* Filters */}
<div className="flex flex-col sm:flex-row gap-4">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder={__('Search license keys...')}
value={search}
onChange={(e) => {
setSearch(e.target.value);
setPage(1);
}}
className="!pl-9"
/>
</div>
<Select value={status} onValueChange={(v) => { setStatus(v); setPage(1); }}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder={__('All Statuses')} />
</SelectTrigger>
<SelectContent>
<SelectItem value="">{__('All Statuses')}</SelectItem>
<SelectItem value="active">{__('Active')}</SelectItem>
<SelectItem value="revoked">{__('Revoked')}</SelectItem>
</SelectContent>
</Select>
</div>
{/* Table */}
<div className="border rounded-lg">
<Table>
<TableHeader>
<TableRow>
<TableHead>{__('License Key')}</TableHead>
<TableHead>{__('Product')}</TableHead>
<TableHead>{__('Customer')}</TableHead>
<TableHead>{__('Activations')}</TableHead>
<TableHead>{__('Status')}</TableHead>
<TableHead>{__('Expires')}</TableHead>
<TableHead className="w-[100px]">{__('Actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
{__('Loading...')}
</TableCell>
</TableRow>
) : data?.licenses.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
{__('No licenses found')}
</TableCell>
</TableRow>
) : (
data?.licenses.map((license) => (
<TableRow key={license.id}>
<TableCell>
<div className="flex items-center gap-2">
<code className="text-xs bg-muted px-2 py-1 rounded font-mono">
{license.license_key}
</code>
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => copyToClipboard(license.license_key)}
>
{copiedKey === license.license_key ? (
<Check className="h-3 w-3 text-green-500" />
) : (
<Copy className="h-3 w-3" />
)}
</Button>
</div>
</TableCell>
<TableCell>
<span className="font-medium">{license.product_name}</span>
</TableCell>
<TableCell>
<div>
<p className="font-medium">{license.user_name}</p>
<p className="text-xs text-muted-foreground">{license.user_email}</p>
</div>
</TableCell>
<TableCell>
<span className={license.activations_remaining === 0 ? 'text-red-500' : ''}>
{license.activation_count} / {license.activation_limit === 0 ? '∞' : license.activation_limit}
</span>
</TableCell>
<TableCell>{getStatusBadge(license)}</TableCell>
<TableCell>
{license.expires_at ? (
<span className={license.is_expired ? 'text-red-500' : ''}>
{new Date(license.expires_at).toLocaleDateString()}
</span>
) : (
<span className="text-muted-foreground">{__('Never')}</span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
onClick={() => navigate(`/products/licenses/${license.id}`)}
>
<Eye className="h-4 w-4" />
</Button>
{license.status === 'active' && (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="icon">
<Ban className="h-4 w-4 text-destructive" />
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{__('Revoke License')}</AlertDialogTitle>
<AlertDialogDescription>
{__('This will permanently revoke the license. The customer will no longer be able to use it.')}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{__('Cancel')}</AlertDialogCancel>
<AlertDialogAction
onClick={() => revokeMutation.mutate(license.id)}
className="bg-destructive text-destructive-foreground"
>
{__('Revoke')}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</div>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{totalPages > 1 && (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
{__('Showing')} {((page - 1) * 20) + 1} - {Math.min(page * 20, data?.total || 0)} {__('of')} {data?.total || 0}
</p>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setPage(p => Math.max(1, p - 1))}
disabled={page === 1}
>
{__('Previous')}
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setPage(p => Math.min(totalPages, p + 1))}
disabled={page === totalPages}
>
{__('Next')}
</Button>
</div>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,258 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '@/lib/api/client';
import { Button } from '@/components/ui/button';
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from '@/components/ui/alert-dialog';
import { Key, Copy, Check, ChevronDown, ChevronUp, Monitor, Globe, Power } from 'lucide-react';
import { toast } from 'sonner';
interface Activation {
id: number;
domain: string | null;
machine_id: string | null;
ip_address: string | null;
status: 'active' | 'deactivated';
activated_at: string;
}
interface License {
id: number;
license_key: string;
product_name: string;
status: 'active' | 'revoked' | 'expired';
activation_limit: number;
activation_count: number;
activations_remaining: number;
expires_at: string | null;
is_expired: boolean;
created_at: string;
activations: Activation[];
}
export default function Licenses() {
const queryClient = useQueryClient();
const [copiedKey, setCopiedKey] = useState<string | null>(null);
const [expandedLicense, setExpandedLicense] = useState<number | null>(null);
const { data: licenses = [], isLoading } = useQuery<License[]>({
queryKey: ['account-licenses'],
queryFn: () => api.get('/account/licenses'),
});
const deactivateMutation = useMutation({
mutationFn: ({ licenseId, activationId }: { licenseId: number; activationId: number }) =>
api.post(`/account/licenses/${licenseId}/deactivate`, { activation_id: activationId }),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['account-licenses'] });
toast.success('Activation deactivated successfully');
},
onError: () => {
toast.error('Failed to deactivate');
},
});
const copyToClipboard = (key: string) => {
navigator.clipboard.writeText(key);
setCopiedKey(key);
setTimeout(() => setCopiedKey(null), 2000);
toast.success('License key copied to clipboard');
};
const getStatusStyle = (license: License) => {
if (license.status === 'revoked') {
return 'bg-red-100 text-red-800';
}
if (license.is_expired) {
return 'bg-gray-100 text-gray-800';
}
return 'bg-green-100 text-green-800';
};
const getStatusLabel = (license: License) => {
if (license.status === 'revoked') return 'Revoked';
if (license.is_expired) return 'Expired';
return 'Active';
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Key className="h-6 w-6" />
My Licenses
</h1>
<p className="text-gray-500">
Manage your software licenses and activations
</p>
</div>
{licenses.length === 0 ? (
<div className="bg-white rounded-lg border p-12 text-center">
<Key className="h-12 w-12 mx-auto mb-4 text-gray-300" />
<p className="text-gray-500">You don't have any licenses yet.</p>
<p className="text-sm text-gray-400 mt-1">
Purchase a product with licensing to get started.
</p>
</div>
) : (
<div className="space-y-4">
{licenses.map((license) => (
<div key={license.id} className="bg-white rounded-lg border overflow-hidden">
{/* Header */}
<div className="p-4">
<div className="flex items-start justify-between">
<div className="space-y-1">
<h3 className="text-lg font-semibold">{license.product_name}</h3>
<div className="flex items-center gap-2">
<code className="text-xs bg-gray-100 px-2 py-1 rounded font-mono">
{license.license_key}
</code>
<button
onClick={() => copyToClipboard(license.license_key)}
className="p-1 hover:bg-gray-100 rounded"
>
{copiedKey === license.license_key ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4 text-gray-400" />
)}
</button>
</div>
</div>
<div className="flex items-center gap-2">
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusStyle(license)}`}>
{getStatusLabel(license)}
</span>
<button
onClick={() => setExpandedLicense(expandedLicense === license.id ? null : license.id)}
className="p-1 hover:bg-gray-100 rounded"
>
{expandedLicense === license.id ? (
<ChevronUp className="h-5 w-5 text-gray-400" />
) : (
<ChevronDown className="h-5 w-5 text-gray-400" />
)}
</button>
</div>
</div>
{/* Summary Info */}
<div className="grid grid-cols-3 gap-4 mt-4 text-sm">
<div>
<p className="text-gray-500">Activations</p>
<p className="font-medium">
{license.activation_count} / {license.activation_limit === 0 ? '' : license.activation_limit}
</p>
</div>
<div>
<p className="text-gray-500">Purchased</p>
<p className="font-medium">
{new Date(license.created_at).toLocaleDateString()}
</p>
</div>
<div>
<p className="text-gray-500">Expires</p>
<p className={`font-medium ${license.is_expired ? 'text-red-500' : ''}`}>
{license.expires_at
? new Date(license.expires_at).toLocaleDateString()
: 'Never'}
</p>
</div>
</div>
</div>
{/* Expanded Content - Activations */}
{expandedLicense === license.id && (
<div className="border-t p-4 bg-gray-50">
<h4 className="font-medium mb-3">Active Devices</h4>
{license.activations.filter(a => a.status === 'active').length === 0 ? (
<p className="text-sm text-gray-500 py-4 text-center">
No active devices. Activate your license on a device to see it here.
</p>
) : (
<div className="space-y-2">
{license.activations
.filter(a => a.status === 'active')
.map((activation) => (
<div
key={activation.id}
className="flex items-center justify-between bg-white p-3 rounded border"
>
<div className="flex items-center gap-2">
{activation.domain ? (
<>
<Globe className="h-4 w-4 text-gray-400" />
<span className="text-sm">{activation.domain}</span>
</>
) : activation.machine_id ? (
<>
<Monitor className="h-4 w-4 text-gray-400" />
<span className="text-sm font-mono">
{activation.machine_id.substring(0, 16)}...
</span>
</>
) : (
<span className="text-gray-400 text-sm">Unknown device</span>
)}
<span className="text-gray-400 text-xs">
{new Date(activation.activated_at).toLocaleDateString()}
</span>
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="ghost" size="sm" className="text-red-500 hover:text-red-600">
<Power className="h-4 w-4 mr-1" />
Deactivate
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Deactivate Device</AlertDialogTitle>
<AlertDialogDescription>
This will deactivate the license on this device. You can reactivate it later if you have available activation slots.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
onClick={() => deactivateMutation.mutate({
licenseId: license.id,
activationId: activation.id,
})}
>
Deactivate
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
))}
</div>
)}
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -1,6 +1,6 @@
import React, { ReactNode, useState, useEffect } from 'react'; import React, { ReactNode, useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom'; import { Link, useLocation } from 'react-router-dom';
import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut } from 'lucide-react'; import { LayoutDashboard, ShoppingBag, Download, MapPin, Heart, User, LogOut, Key } from 'lucide-react';
import { useModules } from '@/hooks/useModules'; import { useModules } from '@/hooks/useModules';
import { api } from '@/lib/api/client'; import { api } from '@/lib/api/client';
import { import {
@@ -53,13 +53,16 @@ export function AccountLayout({ children }: AccountLayoutProps) {
{ id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download }, { id: 'downloads', label: 'Downloads', path: '/my-account/downloads', icon: Download },
{ id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin }, { id: 'addresses', label: 'Addresses', path: '/my-account/addresses', icon: MapPin },
{ id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart }, { id: 'wishlist', label: 'Wishlist', path: '/my-account/wishlist', icon: Heart },
{ id: 'licenses', label: 'Licenses', path: '/my-account/licenses', icon: Key },
{ id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User }, { id: 'account-details', label: 'Account Details', path: '/my-account/account-details', icon: User },
]; ];
// Filter out wishlist if module disabled or settings disabled // Filter out wishlist if module disabled or settings disabled, licenses if licensing disabled
const menuItems = allMenuItems.filter(item => const menuItems = allMenuItems.filter(item => {
item.id !== 'wishlist' || (isEnabled('wishlist') && wishlistEnabled) if (item.id === 'wishlist') return isEnabled('wishlist') && wishlistEnabled;
); if (item.id === 'licenses') return isEnabled('licensing');
return true;
});
const handleLogout = async () => { const handleLogout = async () => {
setIsLoggingOut(true); setIsLoggingOut(true);

View File

@@ -9,6 +9,7 @@ import Downloads from './Downloads';
import Addresses from './Addresses'; import Addresses from './Addresses';
import Wishlist from './Wishlist'; import Wishlist from './Wishlist';
import AccountDetails from './AccountDetails'; import AccountDetails from './AccountDetails';
import Licenses from './Licenses';
export default function Account() { export default function Account() {
const user = (window as any).woonoowCustomer?.user; const user = (window as any).woonoowCustomer?.user;
@@ -30,6 +31,7 @@ export default function Account() {
<Route path="downloads" element={<Downloads />} /> <Route path="downloads" element={<Downloads />} />
<Route path="addresses" element={<Addresses />} /> <Route path="addresses" element={<Addresses />} />
<Route path="wishlist" element={<Wishlist />} /> <Route path="wishlist" element={<Wishlist />} />
<Route path="licenses" element={<Licenses />} />
<Route path="account-details" element={<AccountDetails />} /> <Route path="account-details" element={<AccountDetails />} />
<Route path="*" element={<Navigate to="/my-account" replace />} /> <Route path="*" element={<Navigate to="/my-account" replace />} />
</Routes> </Routes>
@@ -37,3 +39,4 @@ export default function Account() {
</Container> </Container>
); );
} }