Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions web/messages/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@
"misc_secret": "Secret",
"misc_active": "Active",
"misc_disabled": "Disabled",
"misc_expired": "Expired",
"license_business_required": "Available in Business plan.",
"license_upgrade_business_tooltip": "This feature is part of a paid plan.\nUpgrade to Business to activate it.",
"license_enterprise_required": "Available in Enterprise plan.",
"license_upgrade_to_unlock": "Upgrade to unlock.",
"license_no_license": "No license",
"license_plan_usage_business": "Business plan usage",
"license_plan_usage_enterprise": "Enterprise plan usage",
"license_open_source_message": "You're using the open-source version with limited features. Try Business for free to unlock paid features and extended limits.",
"license_see_other_plans": "See other plans",
"license_approaching_limits": "You're approaching the limits of your current plan. To increase your limits, please upgrade to a higher-tier plan.",
"license_capacity_reached": "You've reached your plan's maximum capacity. Upgrade today to avoid interruptions and gain more flexibility.",
"contact_sales": "Contact sales",
"controls_connect": "Connect",
"controls_search": "Search",
"controls_accept": "Accept",
Expand Down
35 changes: 35 additions & 0 deletions web/messages/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,41 @@
"settings_activity_log_streaming_table_stream_type_name": "Destination",
"settings_msg_saved": "Settings saved",
"settings_msg_save_failed": "Failed to save settings",
"settings_license_title": "License management",
"settings_license_subtitle": "Manage your Defguard license, view usage details, and track plan limits.",
"settings_license_current_plan": "Current plan",
"settings_license_no_plan": "No license",
"settings_license_key_title": "License key",
"settings_license_key_description": "Enter your license key to unlock additional Defguard features. Your license key is sent by email after purchase or registration on the Plans page.",
"settings_license_edit_button": "Edit license",
"settings_license_enter_button": "Enter license",
"settings_license_choose_plan_title": "Choose plan that matches your needs",
"settings_license_expand_plan_title": "Expand your possibilities with advanced plans",
"settings_license_compare_our_plans": "Compare our plans",
"settings_license_plan_business_title": "Business",
"settings_license_plan_business_badge": "Most popular",
"settings_license_plan_business_description": "External SSO & SIEM integration, LDAP/Active Directory, Firewall Management, REST API, Real-time clients configuration updates and more!",
"settings_license_plan_business_promotional_copy": "Test all business features of Defguard with up to 5 users and 1 location, allowing you to experience the platform's full capabilities before scaling.",
"settings_license_plan_enterprise_title": "Enterprise",
"settings_license_plan_enterprise_description": "Expand your security with: High Availability, Pre-logon/Always-on VPN, and upcoming support for Device Posture and Hardware based MFA.",
"settings_license_try_business_button": "Try Business for free now",
"settings_license_expired_banner": "Your license key has expired. Please renew your license to continue using Defguard {tier} features.",
"settings_license_expiring_soon_banner": "Your license will expire in {days} days. Please renew your license to avoid losing access to the features included in your current plan.",
"settings_license_expired_notice_title": "License expiration notice",
"settings_license_expired_notice_description_grace_period": "Defguard will continue to work for {duration}, after which it will downgrade to the Open Source version. Please renew your license as soon as possible to avoid losing access to the features included in your current plan.",
"settings_license_expired_notice_description": "Your license has expired. Please renew your license to continue using Defguard {tier} features.",
"settings_license_expired_notice_button": "Update your license now",
"settings_license_unknown": "Unknown",
"settings_license_type_title": "License type",
"settings_license_subscription_type": "Subscription",
"settings_license_offline_type": "Offline",
"settings_license_support_type_title": "Support type",
"settings_license_support_type_value": "Community support",
"settings_license_valid_until_title": "Valid until",
"settings_license_valid_until_with_time_left": "{date} ({duration} left)",
"settings_license_limits_title": "Current plan limits",
"settings_license_users_limit_label": "Added users",
"settings_license_locations_limit_label": "VPN locations",
"settings_smtp_reset_confirm_title": "Reset SMTP Settings",
"settings_smtp_reset_confirm_body": "Are you sure you want to reset SMTP settings? This action cannot be undone.",
"settings_smtp_reset_success": "SMTP settings reset",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import './style.scss';
import { useQuery } from '@tanstack/react-query';
import { Fragment } from 'react/jsx-runtime';
import { LicenseTier } from '../../../../../shared/api/types';
import { m } from '../../../../../paraglide/messages';
import type { LicenseInfo } from '../../../../../shared/api/types';
import { Controls } from '../../../../../shared/components/Controls/Controls';
import { DescriptionBlock } from '../../../../../shared/components/DescriptionBlock/DescriptionBlock';
import { SettingsCard } from '../../../../../shared/components/SettingsCard/SettingsCard';
import { SettingsHeader } from '../../../../../shared/components/SettingsHeader/SettingsHeader';
import { SettingsLayout } from '../../../../../shared/components/SettingsLayout/SettingsLayout';
import { AppText } from '../../../../../shared/defguard-ui/components/AppText/AppText';
import { Badge } from '../../../../../shared/defguard-ui/components/Badge/Badge';
import {
type BadgeProps,
BadgeVariant,
} from '../../../../../shared/defguard-ui/components/Badge/types';
import { Button } from '../../../../../shared/defguard-ui/components/Button/Button';
import { Divider } from '../../../../../shared/defguard-ui/components/Divider/Divider';
import { ExternalLink } from '../../../../../shared/defguard-ui/components/ExternalLink/ExternalLink';
import { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/SizedBox';
import {
TextStyle,
Expand All @@ -29,69 +24,57 @@ import {
getLicenseInfoQueryOptions,
getSettingsQueryOptions,
} from '../../../../../shared/query';
import businessImage from './assets/business.png';
import enterpriseImage from './assets/enterprise.png';
import { getLicenseState, type LicenseState } from '../../../../../shared/utils/license';
import { SettingsLicenseBusinessUpsellSection } from './components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection';
import { SettingsLicenseExpiredNotice } from './components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice';
import { SettingsLicenseInfoSection } from './components/SettingsLicenseInfoSection/SettingsLicenseInfoSection';
import { SettingsLicenseNoLicenseSection } from './components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection';
import { SettingsLicenseModal } from './modals/SettingsLicenseModal/SettingsLicenseModal';

type LicenseItemData = {
imageSrc: string;
title: string;
description: string;
badges?: BadgeProps[];
};

const licenses: Array<LicenseItemData> = [
{
title: 'Business',
imageSrc: businessImage,
description: `Advanced protection, shared access controls, and centralized billing. Ideal for small to medium teams.`,
badges: [{ text: 'Most popular', variant: BadgeVariant.Plan }],
},
{
title: 'Enterprise',
imageSrc: enterpriseImage,
description: `Custom integrations, and dedicated support tailored to your organization’s security and scalability needs.`,
},
];

export const SettingsLicenseTab = () => {
const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions);
const { data: settings } = useQuery(getSettingsQueryOptions);

const licenseTier = licenseInfo?.tier ?? null;
const licenseState = getLicenseState(licenseInfo);

return (
<SettingsLayout id="settings-license-tab">
<SettingsHeader
icon="credit-card"
title="License management"
subtitle="Manage your Defguard license, view usage details and track plan limits."
title={m.settings_license_title()}
subtitle={m.settings_license_subtitle()}
/>
{isPresent(settings) && (
<SettingsCard>
{isPresent(licenseInfo) && (
<SettingsLicenseInfoSection licenseInfo={licenseInfo} />
)}
{isPresent(licenseInfo) &&
isPresent(licenseState) &&
licenseState !== 'noLicense' && (
<SettingsLicenseInfoSection
licenseInfo={licenseInfo}
licenseState={licenseState}
/>
)}
{!isPresent(licenseInfo) && (
<div className="empty-plan">
<AppText font={TextStyle.TBodySm400} color={ThemeVariable.FgNeutral}>
{`Current plan`}
{m.settings_license_current_plan()}
</AppText>
<SizedBox height={ThemeSpacing.Sm} />
<Badge variant="neutral" text={'No plan'} />
<Badge variant="neutral" text={m.settings_license_no_plan()} />
<Divider spacing={ThemeSpacing.Xl} />
</div>
)}
<DescriptionBlock title="License key">
<p>{`Enter your license key to unlock additional Defguard features. Your license key is sent by email after purchase or registration on the Plans page.`}</p>
<DescriptionBlock title={m.settings_license_key_title()}>
<p>{m.settings_license_key_description()}</p>
</DescriptionBlock>
<Controls>
<div className="left">
<Button
variant="primary"
text={
(settings.license?.length ?? 0) > 0 ? 'Edit license' : 'Enter license'
(settings.license?.length ?? 0) > 0
? m.settings_license_edit_button()
: m.settings_license_enter_button()
}
onClick={() => {
openModal(ModalName.SettingsLicense, {
Expand All @@ -103,49 +86,34 @@ export const SettingsLicenseTab = () => {
</Controls>
</SettingsCard>
)}
{isPresent(licenseTier) && !(licenseTier === LicenseTier.Enterprise) && (
<Fragment>
<SizedBox height={ThemeSpacing.Xl} />
<SettingsCard id="license-plans">
<header>
<h5>{`Expand your possibilities with advanced plans`}</h5>
<ExternalLink
href="https://defguard.net/pricing/"
rel="noreferrer noopener"
target="_blank"
>
{`Select your plan`}
</ExternalLink>
</header>
<SizedBox height={ThemeSpacing.Xl3} />
<div className="tiers">
<LicenseItem data={licenses[1]} />
</div>
</SettingsCard>
</Fragment>
)}
<LicenseSection state={licenseState} licenseInfo={licenseInfo} />
<SettingsLicenseModal />
</SettingsLayout>
);
};

const LicenseItem = ({ data }: { data: LicenseItemData }) => {
const LicenseSection = ({
licenseInfo,
state,
}: {
licenseInfo: LicenseInfo | null | undefined;
state: LicenseState | null;
}) => {
if (state === null || state === 'validEnterprise') {
return null;
}

return (
<div className="license-item">
<div className="track">
<div className="image-track">
<img src={data.imageSrc} />
</div>
<div className="content">
<div className="top">
<p className="title">{data.title}</p>
{data.badges?.map((props) => (
<Badge {...props} key={props.text} />
))}
</div>
<p className="description">{data.description}</p>
</div>
</div>
</div>
<>
<SizedBox height={ThemeSpacing.Xl} />
{state === 'noLicense' && <SettingsLicenseNoLicenseSection />}
{state === 'gracePeriod' && isPresent(licenseInfo) && (
<SettingsLicenseExpiredNotice licenseInfo={licenseInfo} state="gracePeriod" />
)}
{state === 'expiredLicense' && isPresent(licenseInfo) && (
<SettingsLicenseExpiredNotice licenseInfo={licenseInfo} state="expiredLicense" />
)}
{state === 'validBusiness' && <SettingsLicenseBusinessUpsellSection />}
</>
);
};
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { m } from '../../../../../../../paraglide/messages';
import { SettingsCard } from '../../../../../../../shared/components/SettingsCard/SettingsCard';
import { externalLink } from '../../../../../../../shared/constants';
import { Button } from '../../../../../../../shared/defguard-ui/components/Button/Button';
import { ExternalLink } from '../../../../../../../shared/defguard-ui/components/ExternalLink/ExternalLink';
import { SizedBox } from '../../../../../../../shared/defguard-ui/components/SizedBox/SizedBox';
import { ThemeSpacing } from '../../../../../../../shared/defguard-ui/types';
import enterpriseImage from '../../assets/enterprise.png';

export const SettingsLicenseBusinessUpsellSection = () => {
return (
<SettingsCard id="license-plans">
<header>
<h5>{m.settings_license_expand_plan_title()}</h5>
<ExternalLink
href={externalLink.defguard.pricing}
rel="noreferrer noopener"
target="_blank"
>
{m.settings_license_compare_our_plans()}
</ExternalLink>
</header>
<SizedBox height={ThemeSpacing.Xl3} />
<div className="license-item">
<div className="track">
<div className="image-track">
<img src={enterpriseImage} alt="" />
</div>
<div className="content">
<div className="top">
<p className="title">{m.settings_license_plan_enterprise_title()}</p>
</div>
<p className="description">
{m.settings_license_plan_enterprise_description()}
</p>
<SizedBox height={ThemeSpacing.Md} />
<div className="actions">
<a
href={externalLink.defguard.sales}
rel="noreferrer noopener"
target="_blank"
>
<Button variant="outlined" text={m.contact_sales()} />
</a>
</div>
</div>
</div>
</div>
</SettingsCard>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import dayjs from 'dayjs';
import { m } from '../../../../../../../paraglide/messages';
import type { LicenseInfo } from '../../../../../../../shared/api/types';
import { SettingsCard } from '../../../../../../../shared/components/SettingsCard/SettingsCard';
import {
externalLink,
licenseGracePeriodDays,
} from '../../../../../../../shared/constants';
import { Button } from '../../../../../../../shared/defguard-ui/components/Button/Button';
import expiredImage from '../../assets/expired.png';

type Props = {
licenseInfo: LicenseInfo;
state: 'gracePeriod' | 'expiredLicense';
};

export const SettingsLicenseExpiredNotice = ({ licenseInfo, state }: Props) => {
const gracePeriodDaysLeft = getGracePeriodDaysLeft(licenseInfo.valid_until);

const remainingDuration = m.settings_duration_days({ days: gracePeriodDaysLeft });

const description =
state === 'expiredLicense'
? m.settings_license_expired_notice_description({ tier: licenseInfo.tier })
: m.settings_license_expired_notice_description_grace_period({
duration: remainingDuration,
});

return (
<SettingsCard id="license-expired-notice">
<div className="notice-track">
<div className="image-track">
<img src={expiredImage} alt="" />
</div>
<div className="content-track">
<p className="title">{m.settings_license_expired_notice_title()}</p>
<p className="description">{description}</p>
<a
href={externalLink.defguard.pricing}
rel="noreferrer noopener"
target="_blank"
>
<Button
variant="outlined"
text={m.settings_license_expired_notice_button()}
/>
</a>
</div>
</div>
</SettingsCard>
);
};

const getGracePeriodDaysLeft = (validUntil: string | null): number => {
const gracePeriodEndsAt = validUntil
? dayjs.utc(validUntil).local().add(licenseGracePeriodDays, 'day')
: null;

return gracePeriodEndsAt
? Math.max(gracePeriodEndsAt.startOf('day').diff(dayjs().startOf('day'), 'day'), 0)
: 0;
};
Loading
Loading