mirror of
https://github.com/documenso/documenso.git
synced 2026-06-22 04:12:06 +10:00
224 lines
7.6 KiB
TypeScript
224 lines
7.6 KiB
TypeScript
import type { TCachedLicense } from '@documenso/lib/types/license';
|
|
import { SUBSCRIPTION_CLAIM_FEATURE_FLAGS } from '@documenso/lib/types/subscription';
|
|
import { trpc } from '@documenso/trpc/react';
|
|
import { Badge } from '@documenso/ui/primitives/badge';
|
|
import { Button } from '@documenso/ui/primitives/button';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@documenso/ui/primitives/tooltip';
|
|
import { useToast } from '@documenso/ui/primitives/use-toast';
|
|
import { Trans, useLingui } from '@lingui/react/macro';
|
|
import {
|
|
ArrowRightIcon,
|
|
CheckCircle2Icon,
|
|
EyeIcon,
|
|
EyeOffIcon,
|
|
KeyRoundIcon,
|
|
Loader2Icon,
|
|
RefreshCwIcon,
|
|
XCircleIcon,
|
|
} from 'lucide-react';
|
|
import { DateTime } from 'luxon';
|
|
import { useState } from 'react';
|
|
import { Link, useRevalidator } from 'react-router';
|
|
import { match } from 'ts-pattern';
|
|
|
|
import { CardMetric } from './metric-card';
|
|
|
|
type AdminLicenseCardProps = {
|
|
licenseData: TCachedLicense | null;
|
|
};
|
|
|
|
export const AdminLicenseCard = ({ licenseData }: AdminLicenseCardProps) => {
|
|
const { t, i18n } = useLingui();
|
|
const [isLicenseKeyVisible, setIsLicenseKeyVisible] = useState(false);
|
|
|
|
const { license } = licenseData || {};
|
|
|
|
if (!license) {
|
|
return (
|
|
<div className="relative">
|
|
<div className="absolute top-3 right-3 z-10">
|
|
<AdminLicenseResyncButton />
|
|
</div>
|
|
<CardMetric icon={KeyRoundIcon} title={t`License`} className="h-fit max-h-fit">
|
|
<div className="mt-1 flex items-center justify-center gap-2">
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg border border-muted-foreground/30 border-dashed bg-muted/50">
|
|
<KeyRoundIcon className="h-5 w-5 text-muted-foreground/50" />
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-0.5">
|
|
{licenseData?.requestedLicenseKey ? (
|
|
<>
|
|
<p className="font-medium text-destructive text-sm">
|
|
<Trans>Invalid License Key</Trans>
|
|
</p>
|
|
{/* Don't need to hide invalid license keys. */}
|
|
<p className="text-muted-foreground text-xs">{licenseData.requestedLicenseKey}</p>
|
|
</>
|
|
) : (
|
|
<p className="font-medium text-muted-foreground text-sm">
|
|
<Trans>No License Configured</Trans>
|
|
</p>
|
|
)}
|
|
|
|
<Link
|
|
to="https://docs.documenso.com/users/licenses/enterprise-edition"
|
|
target="_blank"
|
|
className="flex flex-row items-center text-muted-foreground text-xs hover:text-muted-foreground/80"
|
|
>
|
|
<Trans>Learn more</Trans> <ArrowRightIcon className="h-3 w-3" />
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
</CardMetric>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const enabledFlags = Object.entries(license.flags).filter(([, enabled]) => enabled);
|
|
|
|
return (
|
|
<div className="relative max-w-full overflow-hidden rounded-lg border border-border bg-background px-4 pt-4 pb-6 shadow shadow-transparent duration-200 hover:shadow-border/80">
|
|
<div className="absolute top-3 right-3">
|
|
<AdminLicenseResyncButton />
|
|
</div>
|
|
|
|
<div className="flex items-start gap-2">
|
|
<div className="h-4 w-4">
|
|
<KeyRoundIcon className="h-4 w-4 text-muted-foreground" />
|
|
</div>
|
|
|
|
<h3 className="mb-2 flex items-end font-medium text-primary-forground text-sm leading-tight">
|
|
<Trans>Documenso License</Trans>
|
|
</h3>
|
|
|
|
{match(license.status)
|
|
.with('ACTIVE', () => (
|
|
<Badge variant="default" size="small">
|
|
<CheckCircle2Icon className="mr-1 h-3 w-3" />
|
|
<Trans context="Subscription status">Active</Trans>
|
|
</Badge>
|
|
))
|
|
.with('PAST_DUE', () => (
|
|
<Badge variant="warning" size="small">
|
|
<XCircleIcon className="mr-1 h-3 w-3" />
|
|
<Trans context="Subscription status">Past Due</Trans>
|
|
</Badge>
|
|
))
|
|
.with('EXPIRED', () => (
|
|
<Badge variant="destructive" size="small">
|
|
<XCircleIcon className="mr-1 h-3 w-3" />
|
|
<Trans context="Subscription status">Expired</Trans>
|
|
</Badge>
|
|
))
|
|
.otherwise(() => null)}
|
|
</div>
|
|
|
|
<div className="mt-4 grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p className="font-medium text-foreground text-sm">
|
|
<Trans>License</Trans>
|
|
</p>
|
|
<p className="mt-0.5 text-muted-foreground text-xs">{license.name}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="font-medium text-foreground text-sm">
|
|
<Trans>Expires</Trans>
|
|
</p>
|
|
<p className="mt-0.5 text-muted-foreground text-xs">{i18n.date(license.periodEnd, DateTime.DATE_MED)}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="font-medium text-foreground text-sm">
|
|
<Trans>License Key</Trans>
|
|
</p>
|
|
<div className="mt-0.5 flex items-center gap-1">
|
|
<p className="min-w-0 break-all text-muted-foreground text-xs">
|
|
{isLicenseKeyVisible ? license.licenseKey : '•'.repeat(license.licenseKey.length)}
|
|
</p>
|
|
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 w-6 p-0 text-muted-foreground"
|
|
aria-label={isLicenseKeyVisible ? t`Hide license key` : t`Show license key`}
|
|
onClick={() => setIsLicenseKeyVisible((prevState) => !prevState)}
|
|
>
|
|
{isLicenseKeyVisible ? <EyeOffIcon className="h-3.5 w-3.5" /> : <EyeIcon className="h-3.5 w-3.5" />}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="font-medium text-foreground text-sm">
|
|
<Trans>Features</Trans>
|
|
</p>
|
|
<p className="mt-0.5 text-muted-foreground text-xs">
|
|
{enabledFlags.length > 0 ? (
|
|
enabledFlags
|
|
.map(
|
|
([flag]) =>
|
|
SUBSCRIPTION_CLAIM_FEATURE_FLAGS[
|
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
flag as keyof typeof SUBSCRIPTION_CLAIM_FEATURE_FLAGS
|
|
]?.label || flag,
|
|
)
|
|
.join(', ')
|
|
) : (
|
|
<Trans>No features enabled</Trans>
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const AdminLicenseResyncButton = () => {
|
|
const { t } = useLingui();
|
|
const { toast } = useToast();
|
|
const { revalidate } = useRevalidator();
|
|
|
|
const { mutate: resyncLicense, isPending: isResyncingLicense } = trpc.admin.license.resync.useMutation({
|
|
onSuccess: async () => {
|
|
toast({
|
|
title: t`License synced`,
|
|
});
|
|
|
|
await revalidate();
|
|
},
|
|
onError: () => {
|
|
toast({
|
|
title: t`Failed to sync license`,
|
|
variant: 'destructive',
|
|
});
|
|
},
|
|
});
|
|
|
|
return (
|
|
<TooltipProvider>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 w-8 p-0"
|
|
disabled={isResyncingLicense}
|
|
onClick={() => resyncLicense()}
|
|
>
|
|
{isResyncingLicense ? (
|
|
<Loader2Icon className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<RefreshCwIcon className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<Trans>Sync license from server</Trans>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</TooltipProvider>
|
|
);
|
|
};
|