From f55f23a17dca1cf7da5b7dcc77750d5f7c250e0e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 18 Mar 2026 12:59:45 +0100 Subject: [PATCH 01/21] license upsell section - initial implementation - refactor license settings tab into explicit UI states for: - no license - expired license - valid business - valid enterprise - extract promotional and expired-notice markup into dedicated components - add no-license and expired-license upgrade sections - update license details to show "Valid until" for expired licenses and align copy with the approved design - migrate new license-tab copy to Paraglide messages and replace hardcoded pricing URLs with shared constants - remove stale frontend Starter tier/copy so no-license is represented consistently as null --- web/messages/en/common.json | 8 + web/messages/en/settings.json | 30 +++- .../SettingsLicenseTab/SettingsLicenseTab.tsx | 145 ++++++++++-------- .../SettingsLicenseExpiredNotice.tsx | 32 ++++ .../SettingsLicenseInfoSection.tsx | 35 +++-- .../SettingsLicensePlansSection.tsx | 105 +++++++++++++ .../tabs/SettingsLicenseTab/style.scss | 56 +++++++ web/src/shared/api/types.ts | 1 - .../TopBarLicense/TopBarLicenseFloating.tsx | 25 +-- 9 files changed, 344 insertions(+), 93 deletions(-) create mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx create mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicensePlansSection/SettingsLicensePlansSection.tsx diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 8af5a20a1d..0934dc0f19 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -10,10 +10,18 @@ "misc_secret": "Secret", "misc_active": "Active", "misc_disabled": "Disabled", + "state_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.", "controls_connect": "Connect", "controls_search": "Search", "controls_accept": "Accept", diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 4d54ebb8d9..4f3dcd4035 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -48,5 +48,33 @@ "settings_activity_log_streaming_table_header_name": "Name", "settings_activity_log_streaming_table_stream_type_name": "Destination", "settings_msg_saved": "Settings saved", - "settings_msg_save_failed": "Failed to save settings" + "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_select_plan": "Select your plan", + "settings_license_plan_business_description": "Advanced protection, shared access controls, and centralized billing - ideal for small to medium teams.", + "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_description": "Custom integrations, and dedicated support tailored to your organization's security and scalability needs.", + "settings_license_try_business_button": "Try Business for free now", + "settings_license_expired_notice_title": "License expiration notice", + "settings_license_expired_notice_description": "Your license has expired. Renew it to continue using paid Defguard features and extended plan limits.", + "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_limits_title": "Current plan limits", + "settings_license_users_limit_label": "Added users", + "settings_license_locations_limit_label": "VPN locations" } diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx index 0ac2e4d8b1..230e7c7622 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx @@ -1,7 +1,7 @@ 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'; @@ -9,13 +9,9 @@ import { SettingsHeader } from '../../../../../shared/components/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 { 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, @@ -31,42 +27,71 @@ import { } from '../../../../../shared/query'; import businessImage from './assets/business.png'; import enterpriseImage from './assets/enterprise.png'; +import { SettingsLicenseExpiredNotice } from './components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice'; import { SettingsLicenseInfoSection } from './components/SettingsLicenseInfoSection/SettingsLicenseInfoSection'; +import { + type LicensePlanCardData, + SettingsLicensePlansSection, +} from './components/SettingsLicensePlansSection/SettingsLicensePlansSection'; import { SettingsLicenseModal } from './modals/SettingsLicenseModal/SettingsLicenseModal'; -type LicenseItemData = { - imageSrc: string; - title: string; - description: string; - badges?: BadgeProps[]; -}; +type LicenseSectionState = + | 'expiredLicense' + | 'noLicense' + | 'validBusiness' + | 'validEnterprise'; -const licenses: Array = [ +const licenseCards: Array = [ { - title: 'Business', + badges: [{ text: m.misc_recommended(), variant: BadgeVariant.Plan }], + description: m.settings_license_plan_business_description(), imageSrc: businessImage, - description: `Advanced protection, shared access controls, and centralized billing. Ideal for small to medium teams.`, - badges: [{ text: 'Most popular', variant: BadgeVariant.Plan }], + promotionalCopy: m.settings_license_plan_business_promotional_copy(), + tier: 'Business', + title: 'Business', }, { - title: 'Enterprise', + description: m.settings_license_plan_enterprise_description(), imageSrc: enterpriseImage, - description: `Custom integrations, and dedicated support tailored to your organization’s security and scalability needs.`, + tier: 'Enterprise', + title: 'Enterprise', }, ]; +const getLicenseSectionState = ( + licenseInfo: LicenseInfo | null | undefined, +): LicenseSectionState | null => { + if (licenseInfo === undefined) { + return null; + } + + if (licenseInfo === null) { + return 'noLicense'; + } + + if (licenseInfo.expired) { + return 'expiredLicense'; + } + + if (licenseInfo.tier === 'Enterprise') { + return 'validEnterprise'; + } + + return 'validBusiness'; +}; + export const SettingsLicenseTab = () => { const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions); const { data: settings } = useQuery(getSettingsQueryOptions); - const licenseTier = licenseInfo?.tier ?? null; + const sectionState = getLicenseSectionState(licenseInfo); return ( {isPresent(settings) && ( @@ -76,22 +101,24 @@ export const SettingsLicenseTab = () => { {!isPresent(licenseInfo) && (
- {`Current plan`} + {m.settings_license_current_plan()} - +
)} - -

{`Enter your license key to unlock additional Defguard features. Your license key is sent by email after purchase or registration on the Plans page.`}

+ +

{m.settings_license_key_description()}

+ +
+ ); +}; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index a6b3058fb4..ebe47a48df 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -1,6 +1,7 @@ import { Fragment, type PropsWithChildren, useMemo } from 'react'; import './style.scss'; import dayjs from 'dayjs'; +import { m } from '../../../../../../../paraglide/messages'; import type { LicenseInfo, LicenseLimitsInfo, @@ -25,28 +26,32 @@ export const SettingsLicenseInfoSection = ({ licenseInfo: license }: Props) => { return (
- + {isPresent(licenseTier) && ( <>

{licenseTier}

- {license.expired && } - {!license.expired && } + {license.expired && } + {!license.expired && } )} {!isPresent(licenseTier) && (
- +
)}
- -

{license.subscription ? 'Subscription' : 'Offline'}

+ +

+ {license.subscription + ? m.settings_license_subscription_type() + : m.settings_license_offline_type()} +

- -

{`Placeholder`}

+ +

{m.settings_license_support_type_value()}

- {!license.expired && isPresent(license.valid_until) && ( - + {isPresent(license.valid_until) && ( + )} @@ -54,7 +59,7 @@ export const SettingsLicenseInfoSection = ({ licenseInfo: license }: Props) => { {isPresent(license.limits) && ( -

{`Current plan limits`}

+

{m.settings_license_limits_title()}

@@ -72,9 +77,9 @@ const ValidUntil = ({ validUntil }: ValidUntilProps) => { const untilDay = dayjs.utc(validUntil).local(); const nowDay = dayjs(); const diff = untilDay.diff(nowDay, 'days'); - let res = untilDay.format('DD/MM/YYYY'); + let res = untilDay.format('ll'); if (diff > 0 && diff <= 28) { - res += ` (${untilDay.fromNow(true)})`; + res += ` (${untilDay.fromNow(true)} left)`; } return res; }, [validUntil]); @@ -90,13 +95,13 @@ const LimitsSection = ({ limits }: LimitSectionProps) => { return (
{ + return ( + +
+
+ {variant === 'choose' + ? m.settings_license_choose_plan_title() + : m.settings_license_expand_plan_title()} +
+ + {m.settings_license_select_plan()} + +
+ +
+ {cards.map((card) => ( + + + + ))} +
+
+ ); +}; + +type LicenseItemProps = { + data: LicensePlanCardData; + showTryBusinessButton: boolean; +}; + +const LicenseItem = ({ data, showTryBusinessButton }: LicenseItemProps) => { + return ( +
+
+
+ +
+
+
+

{data.title}

+ {data.badges?.map((props) => ( + + ))} +
+

{data.description}

+ {data.promotionalCopy && ( +

{data.promotionalCopy}

+ )} + {showTryBusinessButton && ( + + )} +
+
+
+ ); +}; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss index 8b0703853e..30572b7705 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss @@ -3,13 +3,25 @@ display: flex; flex-flow: row; align-items: center; + gap: var(--spacing-md); & > a { margin-left: auto; } } + .tiers { + display: flex; + flex-flow: column; + } + .license-item { + &:not(:first-child) { + border-top: 1px solid var(--surface-border-primary); + margin-top: var(--spacing-xl2); + padding-top: var(--spacing-xl2); + } + & > .track { display: grid; grid-template-columns: 68px 1fr; @@ -51,6 +63,50 @@ font: var(--t-body-sm-400); color: var(--fg-muted); } + + .promotional-copy { + font: var(--t-body-sm-600); + color: var(--fg-success); + } + + .actions { + display: flex; + flex-flow: row wrap; + gap: var(--spacing-md); + margin-top: var(--spacing-sm); + } + } + } + } +} + +#license-expired-notice { + .notice-track { + display: grid; + grid-template-columns: 80px 1fr; + gap: var(--spacing-xl); + align-items: start; + + .image-track { + img { + width: 100%; + } + } + + .content-track { + display: flex; + flex-flow: column; + align-items: flex-start; + gap: var(--spacing-md); + + .title { + color: var(--fg-critical); + font: var(--t-title-h5); + } + + .description { + color: var(--fg-default); + font: var(--t-body-primary-400); } } } diff --git a/web/src/shared/api/types.ts b/web/src/shared/api/types.ts index 4a74897b19..7f07bb270d 100644 --- a/web/src/shared/api/types.ts +++ b/web/src/shared/api/types.ts @@ -365,7 +365,6 @@ export interface LicenseCheckResponse { } export const LicenseTier = { - Starter: 'Starter', Business: 'Business', Enterprise: 'Enterprise', } as const; diff --git a/web/src/shared/components/PageTopBar/components/TopBarLicense/TopBarLicenseFloating.tsx b/web/src/shared/components/PageTopBar/components/TopBarLicense/TopBarLicenseFloating.tsx index c106ac4c6c..21cb57e9e9 100644 --- a/web/src/shared/components/PageTopBar/components/TopBarLicense/TopBarLicenseFloating.tsx +++ b/web/src/shared/components/PageTopBar/components/TopBarLicense/TopBarLicenseFloating.tsx @@ -1,4 +1,5 @@ import { type HTMLProps, useMemo } from 'react'; +import { m } from '../../../../../paraglide/messages'; import type { LicenseInfo } from '../../../../api/types'; import { externalLink } from '../../../../constants'; import { Button } from '../../../../defguard-ui/components/Button/Button'; @@ -29,12 +30,12 @@ export const TopBarLicenseFloating = ({ license, ...props }: Props) => { }, [license]); const title = useMemo(() => { - if (license === null) return 'No License'; + if (license === null) return m.license_no_license(); switch (license.tier) { case 'Business': - return 'Business Plan usage'; + return m.license_plan_usage_business(); case 'Enterprise': - return 'Enterprise Plan usage'; + return m.license_plan_usage_enterprise(); } }, [license]); @@ -49,34 +50,36 @@ export const TopBarLicenseFloating = ({ license, ...props }: Props) => { icon={IconKind.Users} value={license.limits.users.current} maxValue={license.limits.users.limit} - label="Added users" + label={m.settings_license_users_limit_label()} />
)} {!license.limits_exceeded && warning && ( -

{`You're approaching the limits of your current plan. To increase your limits, please upgrade to a higher-tier plan.`}

+

{m.license_approaching_limits()}

)} {license.limits_exceeded && ( -

{`You've reached your plan's maximum capacity. Upgrade today to avoid interruptions and gain more flexibility.`}

+

{m.license_capacity_reached()}

)} )} {!isPresent(license) && ( -

- {`You're using the open-source version with limited features.\n\nTo unlock more flexibility, upgrade your license to “Starter” for free.`} -

+

{m.license_open_source_message()}

)}
From 485a8e5a9a8495ce6adf2a46db156d44256e12e3 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 18 Mar 2026 13:54:02 +0100 Subject: [PATCH 02/21] use the dividers, spacing fixes --- web/messages/en/settings.json | 1 + .../SettingsLicenseTab/SettingsLicenseTab.tsx | 46 +------- .../SettingsLicenseBusinessUpsellSection.tsx | 40 +++++++ .../SettingsLicenseNoLicenseSection.tsx | 83 ++++++++++++++ .../SettingsLicensePlansSection.tsx | 105 ------------------ .../tabs/SettingsLicenseTab/style.scss | 7 -- 6 files changed, 129 insertions(+), 153 deletions(-) create mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx create mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx delete mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicensePlansSection/SettingsLicensePlansSection.tsx diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 4f3dcd4035..3f02ff065c 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -60,6 +60,7 @@ "settings_license_choose_plan_title": "Choose plan that matches your needs", "settings_license_expand_plan_title": "Expand your possibilities with advanced plans", "settings_license_select_plan": "Select your plan", + "settings_license_plan_business_badge": "Most popular", "settings_license_plan_business_description": "Advanced protection, shared access controls, and centralized billing - ideal for small to medium teams.", "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_description": "Custom integrations, and dedicated support tailored to your organization's security and scalability needs.", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx index 230e7c7622..aadf4db5db 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx @@ -9,7 +9,6 @@ import { SettingsHeader } from '../../../../../shared/components/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 { 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 { SizedBox } from '../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; @@ -25,14 +24,10 @@ import { getLicenseInfoQueryOptions, getSettingsQueryOptions, } from '../../../../../shared/query'; -import businessImage from './assets/business.png'; -import enterpriseImage from './assets/enterprise.png'; +import { SettingsLicenseBusinessUpsellSection } from './components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection'; import { SettingsLicenseExpiredNotice } from './components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice'; import { SettingsLicenseInfoSection } from './components/SettingsLicenseInfoSection/SettingsLicenseInfoSection'; -import { - type LicensePlanCardData, - SettingsLicensePlansSection, -} from './components/SettingsLicensePlansSection/SettingsLicensePlansSection'; +import { SettingsLicenseNoLicenseSection } from './components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection'; import { SettingsLicenseModal } from './modals/SettingsLicenseModal/SettingsLicenseModal'; type LicenseSectionState = @@ -41,23 +36,6 @@ type LicenseSectionState = | 'validBusiness' | 'validEnterprise'; -const licenseCards: Array = [ - { - badges: [{ text: m.misc_recommended(), variant: BadgeVariant.Plan }], - description: m.settings_license_plan_business_description(), - imageSrc: businessImage, - promotionalCopy: m.settings_license_plan_business_promotional_copy(), - tier: 'Business', - title: 'Business', - }, - { - description: m.settings_license_plan_enterprise_description(), - imageSrc: enterpriseImage, - tier: 'Enterprise', - title: 'Enterprise', - }, -]; - const getLicenseSectionState = ( licenseInfo: LicenseInfo | null | undefined, ): LicenseSectionState | null => { @@ -144,23 +122,9 @@ const LicenseSection = ({ state }: { state: LicenseSectionState | null }) => { return ( <> - {state === 'expiredLicense' && ( - <> - - - - - )} - {state === 'noLicense' && ( - - )} - {state === 'validBusiness' && ( - - )} + {state === 'expiredLicense' && } + {state === 'noLicense' && } + {state === 'validBusiness' && } ); }; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx new file mode 100644 index 0000000000..af26c9b62b --- /dev/null +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx @@ -0,0 +1,40 @@ +import { m } from '../../../../../../../paraglide/messages'; +import { SettingsCard } from '../../../../../../../shared/components/SettingsCard/SettingsCard'; +import { externalLink } from '../../../../../../../shared/constants'; +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 ( + +
+
{m.settings_license_expand_plan_title()}
+ + {m.settings_license_select_plan()} + +
+ +
+
+
+ +
+
+
+

Enterprise

+
+

+ {m.settings_license_plan_enterprise_description()} +

+
+
+
+
+ ); +}; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx new file mode 100644 index 0000000000..9ad2a30b01 --- /dev/null +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx @@ -0,0 +1,83 @@ +import { m } from '../../../../../../../paraglide/messages'; +import { SettingsCard } from '../../../../../../../shared/components/SettingsCard/SettingsCard'; +import { externalLink } from '../../../../../../../shared/constants'; +import { Badge } from '../../../../../../../shared/defguard-ui/components/Badge/Badge'; +import { 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 { ThemeSpacing } from '../../../../../../../shared/defguard-ui/types'; +import businessImage from '../../assets/business.png'; +import enterpriseImage from '../../assets/enterprise.png'; + +export const SettingsLicenseNoLicenseSection = () => { + return ( + +
+
{m.settings_license_choose_plan_title()}
+ + {m.settings_license_select_plan()} + +
+ +
+
+
+ +
+
+
+

Business

+ +
+

+ {m.settings_license_plan_business_description()} +

+ +

+ {m.settings_license_plan_business_promotional_copy()} +

+ + +
+
+
+ +
+
+
+ +
+
+
+

Enterprise

+
+

+ {m.settings_license_plan_enterprise_description()} +

+
+
+
+
+ ); +}; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicensePlansSection/SettingsLicensePlansSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicensePlansSection/SettingsLicensePlansSection.tsx deleted file mode 100644 index 8946b5cff3..0000000000 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicensePlansSection/SettingsLicensePlansSection.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { Fragment } from 'react'; -import { m } from '../../../../../../../paraglide/messages'; -import { SettingsCard } from '../../../../../../../shared/components/SettingsCard/SettingsCard'; -import { externalLink } from '../../../../../../../shared/constants'; -import { Badge } from '../../../../../../../shared/defguard-ui/components/Badge/Badge'; -import type { BadgeProps } from '../../../../../../../shared/defguard-ui/components/Badge/types'; -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'; - -export type LicensePlanCardData = { - badges?: BadgeProps[]; - description: string; - imageSrc: string; - promotionalCopy?: string; - tier: 'Business' | 'Enterprise'; - title: string; -}; - -type Props = { - cards: LicensePlanCardData[]; - showTryBusinessButton?: boolean; - variant: 'choose' | 'expand'; -}; - -export const SettingsLicensePlansSection = ({ - cards, - showTryBusinessButton = false, - variant, -}: Props) => { - return ( - -
-
- {variant === 'choose' - ? m.settings_license_choose_plan_title() - : m.settings_license_expand_plan_title()} -
- - {m.settings_license_select_plan()} - -
- -
- {cards.map((card) => ( - - - - ))} -
-
- ); -}; - -type LicenseItemProps = { - data: LicensePlanCardData; - showTryBusinessButton: boolean; -}; - -const LicenseItem = ({ data, showTryBusinessButton }: LicenseItemProps) => { - return ( -
-
-
- -
-
-
-

{data.title}

- {data.badges?.map((props) => ( - - ))} -
-

{data.description}

- {data.promotionalCopy && ( -

{data.promotionalCopy}

- )} - {showTryBusinessButton && ( - - )} -
-
-
- ); -}; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss index 30572b7705..063ac59ddf 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/style.scss @@ -16,12 +16,6 @@ } .license-item { - &:not(:first-child) { - border-top: 1px solid var(--surface-border-primary); - margin-top: var(--spacing-xl2); - padding-top: var(--spacing-xl2); - } - & > .track { display: grid; grid-template-columns: 68px 1fr; @@ -73,7 +67,6 @@ display: flex; flex-flow: row wrap; gap: var(--spacing-md); - margin-top: var(--spacing-sm); } } } From 32c153f3e225e64de7b71431213b8b847d6e2d2e Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 18 Mar 2026 14:29:15 +0100 Subject: [PATCH 03/21] expired icon --- .../tabs/SettingsLicenseTab/assets/expired.png | Bin 0 -> 10501 bytes .../tabs/SettingsLicenseTab/assets/starter.png | Bin 17947 -> 0 bytes .../SettingsLicenseExpiredNotice.tsx | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/assets/expired.png delete mode 100644 web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/assets/starter.png diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/assets/expired.png b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/assets/expired.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe89b8d8a2fd74813b44f631ce29ca41ba8a358 GIT binary patch literal 10501 zcmV+gDf-rlP)$5zSBg0;Nb$iK6n7G*Go^lg5th)J|f@>)4w( z{`dNCcfETC-_VqpI+;h&o=gfC!zB%U_;Rb}-4s3ump%nnO0ALFs zG!s!Xd?vR{5HSk`SpaH>Ku2KBLOVYEv+Q;EcU_=ps%(dlX(ga-@R|JI3j)3etHMEi z_$RNgC_pM5`*tJ3^8m1c!9oN;0btGN2?8M%#tA|h38b7QBxjPk5CDOa3~E~zk~(|| z2r?TUB$^#YDoj{gqn9yiTTwm{C#3~RP71gI_eBQtfwLV3j&Hnh7;b?WFdu#P z0H=$^BD9@20X;pvP*_a+)F6lu@tmNj7xaRs3He}=Y9_>8$>G<-LID;g2e5GdeaIi* z3loQa4l^Bpfy(?i06{ZV@L)K#uaCO^(0qdcajQX?ISfS2JdDZ$L8SzX2}0H_l8qH4 zk5z2Q*pYyA6jsr+E$9Fj&mdnR%@9FqqHId3Fq?<|?&Eyz!KZeya&D#E`}huc{KYod z^we(7kBp2!M@J_N<}SnvB^i|z$SkP2Ku*R8kb0wie~JMM^wW=F`jj}Bt@!VLey$rn|xfJP>=v| z8)U%v+l6L-vR%iayiiOO!^f?Z^4uh35AA`rKej^l@E$KOmtk-)2W{;q7XdR0pyLtc zD^So&-O3PXp2%m`Q?X@5tz$10QK@4$5dggW;hMxgEL z^NA{8f?R|E_8#jP6zPOOaeYG&sg5JpVXg#fg`y<)ZP=26sgWF964%1kqw(VWqx-Kr z;mAu5!0ESMfQhq560q=d>5)jWSc2Yj*~Oy|3WZ|or6+}hD3*sPm?26IQlCuLq}F1P z)Yr6C%ZRy7n$GpS3)3I{o)UV106W5>uCEdxZaE}EQ9c;>t}$*oA3c>N<%dCH`X$va zg1;X`G8pr-v0*Zbk+LU_I-8}gPY$VBzh$na=byXbgWqh0{==^_C^mj&8}pDSe>cIL z#1H2uBctQ+X?G8lD;4vns3T@L_^?UxTtf?}_H_qSA; z8%v6}EoDhE^*9Gx4g-R!(`-JuzMLyreUO0?W60_YFr>0nh@o$3QW8f}1xz?ie9f|> z7y6($H{EdS5nGK9U)~D6@BD&+u?SKI!6HfK^y+mRYDny2IbUZITPi_!`-d`HX}#2T-nmei+Hy)Ej2sCk|!`Qb`7dDc%TjM4P5y~atBh;?az1Uej1 zViKH^q}X<8HAN$ryLL@TF)r;*(8j}OH>FaJ%0IDME8P+iNJ;+u%rs7p=3vdb4Q$dI zC_+N_{=lrpH(zYKhG%e)Jo+QC>Vw&HFgrVMAw7^1b91Jq4N#FnB`Tb;LOz)>Ch1e^ zVEpb@b+9mYhUWXkRZ}K&pe{hDbKedQWi6UQ^^NvqZn=o^A03@ghSv{`*zg=1hHD^X5KA} zu_O>I8g9v5fdSr3AYxRPgykxe_At)@py+qM89;<;_{ikQV3Jy#;O9Z4A(n&rhJxcJ zxHj%X_{aiWxHz0%71D@AMdy!neq`b(;(<1QR-N-w=koAJF++&7Fh?^d-_SmG@|KDM z#I1)D8)+HD6N)}Hv_*|#6hwgt#l@&!w1=t=Kp#e3mxZ{7MjM=Cz@LSTnR0`n?59we zn}*x&Yk}Lo)B=r=@iXtmmg8pe9(Iu}Km9vfp^L*~FgB4lg(?Ur73H`fIvMq##RC8Z zmvTO*E7HKNka+I$=xJDVH%Wo)x48wu?H(+p|LHj>zd4 z!ZDH5U=dsl_3B)r$ako}ORUEeVm%rWt#rQe<7)>;r3g^U%RyyWO4cueeAD-&&vX43 z*qZ2kFo!VH9E-aPn;rQjcj7*)Vxgr+1%yV)@$y4ys7>mJOe+H3i)u-1&}POPmbi0WTVKbZNS~x(t0Iu2pFb% z0WN(4Fwzb%-jy7j=W}@u>dF02?u3mGZG%ds!XN=acO(9)2;uM$Anj6sjJvu8(X@j~ z32XFj+gn{Ye-vn8j@L#KHW@iWnREb3=jl)Eli=_Ga0$jsHK_*C#31NNS4M7qsciF8bxsyqh857 zjRoUio|o2p8@6(q`|uzPpM1OCGQOH(wi>MD>yc0fm^!yq=*!Vrsk8vOp;2d_tUo&( zB_55Kl_nz%mbVpIBcW<{fTT`0T_aB@r(H zH1iQZhvHl-x}dtO$&mpir#xXR9J%nvKDG&%jy|pl9Oc3^^t|^Pe6aUf7|)(qc0sLv z=rQF>!)2?0ai3AL;)8F6`snJV@qKu^LCM- zJ(RmGTjxM)Nm@~IkrT3Y7X@r_c8Zjo-uu`NSYcu7VVi*dhF@(c%a-EfgU>v;9SpR@6R1uTR z5N!YjzrV#-T8`2p1O!PS$u$R~Aea1OASF_PA6oB+#A)w`2rhQN4_$}$CzHe<*{m@4 z;60a`|ErnmRqL@V&**-44-B6^0`=scEl#YWr!YC`n8E4RrRkx~iBB>`g zBSLIiFZh`sM+K01H0=`zrNbbGDugQCA=Hru5};(h@I|Cz9%vyC18x7(mD2{3Q@0)t zj`}Ox!UVwq~Sy;dNE^Jz}3Pdpyp=Jm$l=4Ky38Fv|NP+t)I2!-_ZzNr|K+&JD=k@>p2mdK3b8VHKjtOsUGGk&IaO}J#Jy@pk>Y2_)L*a1k zTsUleW9zdCTW)%K%(`>hx_8Gqr}1fZ)GJIzE3?5K)l*67+iLfvdoXg@DR~4jj9tS% zB^>Aq{Z}Wh9Mw)!Ow3Xr0g|;TW`4wDKi$;uS@2%MMV9u!n*OnX+R_kgtbbBja{BcC z9Zo7Xhmv!i)_+n~`UZT;=95ImDT6wmOAyY{gBqNK{s&AgtSH2=`7z`$9^8qyTqS_Z zvY**9=y;AUVx>`RLUV<^>3$=f`d8B@*TF&NYfODRYp0^nj*iE&Rqqf7 zIF8920XkbaVH|9@*P4BR)nc{++!Whz=x)J4GTP8i05CfVUmH0_fE7YtfLLh$tXFR9 ze|M~#38i3mU}QR{B&URz`5M#X-lO7H{5wj!NO}Wlwhwi`js244s^ALH8(mIgfqWJR z!D5h)g^XP0;%vn;nvVg@^xN|m+PN73os-BDlloc(lm^E7TD`={>B8ATQ+p(_MNr84 zH+>lmhva*XM9$# z_LJ#f97@B?P7h5^;~`-6r^ne#Un|Wgt@vr_`_;Yw%E{5Ooz~F^LjyXqvdB_xi&$f= zl)UYPB-Fgjrmw8?192h`_r()~h=eis#er3goTRb&X{D&K9E4!l))2FZHXfaZLetW> zo5O$RMfga_e44s5I#WFC@HMsFdRwJ)5AU5M9@(g2{d(;B&6A@u{0RYK-t=juM>6^;><8B1Y$dc|{Hk5cmNMWOl@&fWhY8xD&&<@T@T=7hGL*kPo zliT7L0NBG4wzs<`!-H=tp)_=N?@h<%3;}sEm&RV8^z99frIifCjFy&dl3p5J;nWag z@qybSzXta^IT2;Qc`?V!o6`US_C(q&ZSls0FrAuG5N9y|n zIUJ2Ol=Ey79gyNQTtKmd5S8?*RR?U~(xIgFh;C*@lm?Ph#`;TX~cIu+Sc9bw-jL z9PxDXfb!etl(~5#Q^1x$zs$BSGDt{I_tpNhacyv1G!tv+pCzqrO-`$$G}sVuIg``! z(iZ|&iT22LYaqkp^jU@zg=^knaFAH=g^JdPz4XptX&Q|5KDsTm-ud?Atfs-|T+D}P zY&?((2vJZP7y>SDa#~#aLcl819!b4CdODz{%*uVylPaI)3X}xLW|K!?ias~OcGOIp ztN|O~>yS>faHPrU=Dl1uvjR%P@+YS+OTcsMaSa4>yf78{MhAx!6LBEHl3kF(09qU6+ufIM}>CFq>K zZ0URTjUvGF1cf$LOM1LNlDB^I2f2RpmLc6a<^7bxl6Md{-1CMb_KG(Dem~V2vJ?jA zl(rD~O|RWl9~>ll9n&3*GMaHZZO!l8{pWls4WndwB%eF$JQR8Bw#cQcMFgx#A84xZ z{PwSJ`AbHq)%VkgF%RVJKgAB>Pev|0@g_g1)CahmN`HV@C0-rq%IgX&9S@xngU2C1 z_4;+3U#YJ5_UqDb*Q=y7EG}y7RT|nO5U}FXcQS{Xc9M6WjNC8Yydt_Uspb+o?ae2t zC?+#yMFcm@Xt~9sjtse5@6<&!KhS+ZQfBAN59dK?SX|Ut-e#^nve!M4c7H4$)o=38 zk}4*Kgn>P!D@qj&NLd?TMy(78F{^y|9wfw_*7h7I4GWGM!z$Ar*=--o(Ku8k<+KO! z9_Dr6&Z-tXP@4Wu4nc@lE%g-RVJsel+tkJ+6`GMdjai{ z{xFrN-IJ`k%&DN|;3U8q>6>Nr_whM=CMz#QPn}v=2EC5!w6>R)(s23OO<6iSXg*hV z^Kmkk{!mE~B_SrX%pDtyl$0wV%dtQLYs;}J5<<(H_upMn8Y-}}$Zl@$pSbZ@e7@Bl z)b&#cP)u|6I`#-o30xtIcja~$_x(G~?Kx2zc$zMxog%yWyFdR;Wj9Ymb~@v-6GR#P zaXynY+kG#sZdy#<_L&mas*oJM z)X1Gk#DHTDPA14lWaP1wZ(A9q!H&@7pfprv-AC#ur7_5)BW27wR1M?z=lO8LD1u%495qW)>49ibOm%CY`^Ngm7n;&=GV04h z*+KI(?U9^=#PUx(`6Qx9t_Djzm=N;^3KyWLNn^_m`<)9-X*j!7)m*ekib4!JGrF&= zdxZJJ&hy%_8l);LdUtYt{o@s-VG*so^erFODYiM_&TYuYdvh~^!!Cd+q6cH(=Rq%# z=K9?grD0L+s0#tB+(81pQsUFYhbwg)N?_WjB)9>YB)pEj)7)B78W!D32w1UB>h+&$ zR3Mz_WF@?um8q?zsj7)8u4eqqAO2Dl93;+kQaA7abwz1d2AhPMAKeylmrESvs4GrT zR!-A2u2b7$jQJ@ zH(5OI^LA55FsA`_sK{0qG(&2ysV^P%rcgv(Or`PV`!32os-`p?A4~x=D|@S!2v(aLF~`@oR!Q=q0W}^-@)G8i$v|)#-7D(zmRN@erjl z=71l9>d1}x;m%l++iq+~<&=iF&vO1K4YM}h7M^nu(8onHmSQdMqL)P3zq0%J2lGaa zoli2P;a$GTZTCajP8(!<_s!GMILY4)l1T6|{esG&WY0hI3++xf~16^wh;cck(VC z@xt!+wqXxHl3_ntQ5w#PWdTDB-AG)cVcoL@4%%eoD2=uMFD<2EWomGGbAX{BBqe4H z>-6IYR;FPld@$&>C24LfEu~>)<`tI;^+5vXDdsq7Gli%Q8(1&l<-m&rD1h?yjnwBB!>s71+fUU$l%I|TA*8U&ouY= zP=@_B31jt?hSgYq1`cvLn` zL-hy$s`@PrE7aI*%3DVLxe#-F7zF0Tr`A3X03gR3xHFg$b)@3tDB$-L0>Ew4yXT z$KL$a@8$L%{~`-*lh8s3wP@V0v<2S3?7nO7YGUK6uJCu>do$x1+vABH|Nke%aU3Va z1(a0$1$EI@s#LKmo0cTdMHdxCmD)|KrlM@CuBtAeK)Z^Nszho8oich9-sIh@Gd!kXga`qX7Q zf8wpGqyeRYao^*=kN#x~?WkDuGwMA`0YtO0E;&aklTvKxU*d`LC##Z%5>zx8wo zj0hwZZ*`pllF|TCZLQe6M*liWdiBZMuT&)sNCM-ooloN7r~ZQ8?cc*4VVOo*UKUFC zGXr`psyN$%o!kxL% zqbg~*gLp9;XgHhl;X%FObU3 z4<(!(b%{`2Vze!)P2y^C55rns3dA6eVjpFeG?XW;uRJ5<7~%DTB_B>5d=6*c+$SJ< z7?ojlU6NWUvXqbh;n5P2R9j&=!9&<}OJvq2$)6p0b=964>)Ww7@;P>RA3i?xT;SXu6-FP6Hu0$VvR+#Fd}E`}eA(p#;x3vi})Oo_PlqW&(OcZs>*Y74aaLo*;&Zvzb-+Aj!IT9N>vhN2w}lD9$q~&isr3L%l}AE{yKS zkw5Ro%`dMYmnN)l4%q+VB;5qh2Z#>o33t-|Q8Ho2UZ?4+6IDq=rSl95fZDcF;qwR( zr~bCrWyaUm`RHo&*&`kwT9`-sY{p~KT%#u!}J8oZJ!@IgwcIJ zM+KSkjCzS}l=8?Q{AqXe+lE%3_0hLBjZ_ea(LK9dsDgi<2hpRb@{ryleDa^amQm7B z{zACQIj&&|RxX|q0piF@yWDOtlk(8poI$?E69-@`zZj6tHi~-?BCu@&1Y@9TyD&I* zqAF=vvX!4_1RkNKkU>XdT3exN3PnCM7CJBlQYE)j^z-b3!>;+=H;Y(weB!R_x;G(W<23_TwZg&ogo< z^sLPxoAF>*0z1R;jFabqjRCRtDWv%T(Ka7M@buI=fa$B3a6tgF?B3(XmshHihW`uC z@X_C19}-d4l!?~y&~wIQ52_adhXO>=$t>;$YzYj`pL~S@&`L-eF1)wDDrvYKo)M(c z-am-W_BQa$6kKs*IN1t;j5NcR*(Te5A9FjU7Y&qoX;*z;UgGHb?9qQyRnl<#*1O}e zWs7Ho*528LuKpnewT*ze0rGs<|L8K`CiW1nn|e3|4=j|E;`>|zAT)g`7D^ei1#>T- zJx0@Cd|s6_Ec;2brSpt@Z3aDU4V+CI!sYjY>;ssc{1|-g9J7%`E7UW{n50zPO2KSE z(zBl#u}8UQj{-zlt?kEdL+h_qB@IioeA@lapA&Suj(c4a43Ue=2NyE|)6x|xnsBM+&g?NVVp|vTG z&Xz(0L< zkkg}7?b1*QJR^nmy=`2V%h;_bbgr14udM2`BPmQfq z-lQg%$NHgdsK+E@>OI&`k6|5y61A~=$KrMcU7`i#OH;Cj* z-E-+IQcVM~C!cxGqmkWL06GW&<43Dq8gBc11_cYA&uDLLD-9q z^0K-m4U6I#jhiHQLP1fU@y4?wIR5g}kq61=^Vqs&Gy3|vkP3Y9^wx9e#F7F*$O9$Z zhaTC{{&2!mQQT;5Dqq{&TKusfB)*>;_>RTai+ZAL#t&!fN@M3nv~1WOdXQBwX;_eF zG;WfAC6sD!{dQ+4NQ%VJnM?+oHxFU+&>(WTY&CUFl&)`?s*%=8rD z=5^@ym!aEPqqQ4Yn`mcRhmh@k5CF!!;V1fw=3M8iPeOK!l*Gc!)v$5 z-{N2<|-uH4ngC z8;7}i&H{tn_rd6Q$#=ugHX`@6@5hkD-f-tqME0OYo1geKgI)@G2#>uY#Bq5f4V4-D zSOU-J?(N6jBljWSP|$s-OOM6$lXXi0X7}0cBGig|*l;oK`T0>b(hCo=oppo63?;Dk9yxNVm7iFiQRtAoJHOdApg09l zs%Zln9{d@+Y@@_HHb1d@5p`14yx~2M%=ILE@tx(6G%O)&TmE=Pp}7mg+wMi*`T_WU z;9?lE-n-kxtog<=tz@q(xtJTXNEE)7D*+G*SoE-mdL?;wE-b+0XOY|VDDvBX z>^z4z{Ny7GmccCLN%+I(PlucY7e>L8r-0@W(3%amFKJNZpiHJ^ATy1N;~BYn3~YM{ zYkGz;d+8$(8{q&#GU1YEFbor?=^?olK&P~Dct#A{g2A;uBuE8DX+_as8pvFl9nDxA;BbU~bKoe+6 zIvDo8Mly`73GN-Ea!3F?A5=2R0zkyiY5+{O)N<2+D4-zUfuOb-es+ypEy<%@G_*&M ze|rE?8!>@jR8SHMbPsZUO`gpvlAy1smlwfY_pdXu@kO6(9=(g&zlKC#rS_7#}1L`@@uqBxiwT1W0n} zL7MLd^G*hD15qCseW+Ubj4%e~F#?Ukr%_DP0UFu8WEcHEi^?d8Q@Z=i00000NkvXX Hu0mjf+*$fd literal 0 HcmV?d00001 diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/assets/starter.png b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/assets/starter.png deleted file mode 100644 index fab257d3955af29790d9c48181a92de131c6e497..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17947 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 WS*uA8V==M-0000 { return ( From c0ce889aa6d3a954bc3ba99cae5f843545b3d7f4 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Wed, 18 Mar 2026 14:59:02 +0100 Subject: [PATCH 04/21] grace period calculations for expired license settings --- web/messages/en/settings.json | 3 +- .../SettingsLicenseTab/SettingsLicenseTab.tsx | 22 +++++++++++-- .../SettingsLicenseExpiredNotice.tsx | 32 +++++++++++++++++-- web/src/shared/constants.ts | 2 ++ 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 3f02ff065c..79c844af70 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -66,7 +66,8 @@ "settings_license_plan_enterprise_description": "Custom integrations, and dedicated support tailored to your organization's security and scalability needs.", "settings_license_try_business_button": "Try Business for free now", "settings_license_expired_notice_title": "License expiration notice", - "settings_license_expired_notice_description": "Your license has expired. Renew it to continue using paid Defguard features and extended plan limits.", + "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_grace_period_ended": "Your grace period has ended and Defguard will downgrade to the Open Source version. Please renew your license as soon as possible to restore access to the features included in your previous plan.", "settings_license_expired_notice_button": "Update your license now", "settings_license_unknown": "Unknown", "settings_license_type_title": "License type", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx index aadf4db5db..0be63ec417 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx @@ -1,5 +1,6 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; +import dayjs from 'dayjs'; import { m } from '../../../../../paraglide/messages'; import type { LicenseInfo } from '../../../../../shared/api/types'; import { Controls } from '../../../../../shared/components/Controls/Controls'; @@ -47,6 +48,13 @@ const getLicenseSectionState = ( return 'noLicense'; } + if ( + licenseInfo.valid_until !== null && + dayjs().isAfter(dayjs.utc(licenseInfo.valid_until).local()) + ) { + return 'expiredLicense'; + } + if (licenseInfo.expired) { return 'expiredLicense'; } @@ -108,13 +116,19 @@ export const SettingsLicenseTab = () => { )} - + ); }; -const LicenseSection = ({ state }: { state: LicenseSectionState | null }) => { +const LicenseSection = ({ + licenseInfo, + state, +}: { + licenseInfo: LicenseInfo | null | undefined; + state: LicenseSectionState | null; +}) => { if (state === null || state === 'validEnterprise') { return null; } @@ -122,7 +136,9 @@ const LicenseSection = ({ state }: { state: LicenseSectionState | null }) => { return ( <> - {state === 'expiredLicense' && } + {state === 'expiredLicense' && isPresent(licenseInfo) && ( + + )} {state === 'noLicense' && } {state === 'validBusiness' && } diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx index abee765d40..25ac6206c6 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx @@ -1,10 +1,36 @@ +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 } from '../../../../../../../shared/constants'; +import { + externalLink, + licenseGracePeriodDays, +} from '../../../../../../../shared/constants'; import { Button } from '../../../../../../../shared/defguard-ui/components/Button/Button'; import expiredImage from '../../assets/expired.png'; -export const SettingsLicenseExpiredNotice = () => { +type Props = { + licenseInfo: LicenseInfo; +}; + +export const SettingsLicenseExpiredNotice = ({ licenseInfo }: Props) => { + const gracePeriodEndsAt = licenseInfo.valid_until + ? dayjs.utc(licenseInfo.valid_until).local().add(licenseGracePeriodDays, 'day') + : null; + + const gracePeriodDaysLeft = gracePeriodEndsAt + ? Math.max(gracePeriodEndsAt.startOf('day').diff(dayjs().startOf('day'), 'day'), 0) + : 0; + + const remainingDuration = m.settings_duration_days({ days: gracePeriodDaysLeft }); + + const description = + gracePeriodDaysLeft > 0 + ? m.settings_license_expired_notice_description_grace_period({ + duration: remainingDuration, + }) + : m.settings_license_expired_notice_description_grace_period_ended(); + return (
@@ -13,7 +39,7 @@ export const SettingsLicenseExpiredNotice = () => {
-

Enterprise

+

{m.settings_license_plan_enterprise_title()}

{m.settings_license_plan_enterprise_description()} diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx index 9ad2a30b01..7bffd0bc69 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx @@ -32,7 +32,7 @@ export const SettingsLicenseNoLicenseSection = () => {

-

Business

+

{m.settings_license_plan_business_title()}

{
-

Enterprise

+

{m.settings_license_plan_enterprise_title()}

{m.settings_license_plan_enterprise_description()} diff --git a/web/src/shared/constants.ts b/web/src/shared/constants.ts index 4727625996..8732b3c6b4 100644 --- a/web/src/shared/constants.ts +++ b/web/src/shared/constants.ts @@ -39,4 +39,4 @@ export const googleProviderBaseUrl = 'https://accounts.google.com'; export const jumpcloudProviderBaseUrl = 'https://oauth.id.jumpcloud.com'; -export const licenseGracePeriodDays = 40; +export const licenseGracePeriodDays = 14; From ce1d4c65064b6c3749d572adafe976e097dff273 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 09:31:38 +0100 Subject: [PATCH 07/21] display grace period as expired --- .../SettingsLicenseTab/SettingsLicenseTab.tsx | 23 +++++++++++-------- .../SettingsLicenseInfoSection.tsx | 14 ++++++++--- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx index e920c797d1..b4f50cfc29 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx @@ -31,16 +31,16 @@ import { SettingsLicenseInfoSection } from './components/SettingsLicenseInfoSect import { SettingsLicenseNoLicenseSection } from './components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection'; import { SettingsLicenseModal } from './modals/SettingsLicenseModal/SettingsLicenseModal'; -type LicenseSectionState = +export type LicenseState = | 'noLicense' | 'gracePeriod' | 'expiredLicense' | 'validBusiness' | 'validEnterprise'; -const getLicenseSectionState = ( +const getLicenseState = ( licenseInfo: LicenseInfo | null | undefined, -): LicenseSectionState | null => { +): LicenseState | null => { if (licenseInfo === undefined) { return null; } @@ -72,7 +72,7 @@ export const SettingsLicenseTab = () => { const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions); const { data: settings } = useQuery(getSettingsQueryOptions); - const sectionState = getLicenseSectionState(licenseInfo); + const licenseState = getLicenseState(licenseInfo); return ( @@ -83,9 +83,14 @@ export const SettingsLicenseTab = () => { /> {isPresent(settings) && ( - {isPresent(licenseInfo) && ( - - )} + {isPresent(licenseInfo) && + isPresent(licenseState) && + licenseState !== 'noLicense' && ( + + )} {!isPresent(licenseInfo) && (

@@ -118,7 +123,7 @@ export const SettingsLicenseTab = () => { )} - + ); @@ -129,7 +134,7 @@ const LicenseSection = ({ state, }: { licenseInfo: LicenseInfo | null | undefined; - state: LicenseSectionState | null; + state: LicenseState | null; }) => { if (state === null || state === 'validEnterprise') { return null; diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index ebe47a48df..e850c65e80 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -16,13 +16,21 @@ import { ProgressionBar } from '../../../../../../../shared/defguard-ui/componen import { SizedBox } from '../../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../../../shared/defguard-ui/types'; import { isPresent } from '../../../../../../../shared/defguard-ui/utils/isPresent'; +import type { LicenseState } from '../../SettingsLicenseTab'; type Props = { licenseInfo: LicenseInfo; + licenseState: LicenseState; }; -export const SettingsLicenseInfoSection = ({ licenseInfo: license }: Props) => { +export const SettingsLicenseInfoSection = ({ + licenseInfo: license, + licenseState, +}: Props) => { const licenseTier = license.tier; + // In the settings UI, grace period should use the expired badge. + const isExpired = licenseState === 'gracePeriod' || licenseState === 'expiredLicense'; + return (
@@ -30,8 +38,8 @@ export const SettingsLicenseInfoSection = ({ licenseInfo: license }: Props) => { {isPresent(licenseTier) && ( <>

{licenseTier}

- {license.expired && } - {!license.expired && } + {isExpired && } + {!isExpired && } )} {!isPresent(licenseTier) && ( From 003f9d48b525c6c8c26ff73e5d8d0e64699fc717 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 09:56:14 +0100 Subject: [PATCH 08/21] extract license types and helpers and reuse them --- .../SettingsLicenseTab/SettingsLicenseTab.tsx | 39 +------------------ .../SettingsLicenseInfoSection.tsx | 2 +- .../TopBarLicenseExpiration.tsx | 26 +++++-------- web/src/shared/utils/license.ts | 38 ++++++++++++++++++ 4 files changed, 49 insertions(+), 56 deletions(-) diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx index b4f50cfc29..d08548f85e 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx @@ -1,6 +1,5 @@ import './style.scss'; import { useQuery } from '@tanstack/react-query'; -import dayjs from 'dayjs'; import { m } from '../../../../../paraglide/messages'; import type { LicenseInfo } from '../../../../../shared/api/types'; import { Controls } from '../../../../../shared/components/Controls/Controls'; @@ -25,49 +24,13 @@ import { getLicenseInfoQueryOptions, getSettingsQueryOptions, } from '../../../../../shared/query'; +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'; -export type LicenseState = - | 'noLicense' - | 'gracePeriod' - | 'expiredLicense' - | 'validBusiness' - | 'validEnterprise'; - -const getLicenseState = ( - licenseInfo: LicenseInfo | null | undefined, -): LicenseState | null => { - if (licenseInfo === undefined) { - return null; - } - - if (licenseInfo === null) { - return 'noLicense'; - } - - if (licenseInfo.expired) { - return 'expiredLicense'; - } - - if ( - licenseInfo.subscription && - licenseInfo.valid_until !== null && - dayjs().isAfter(dayjs.utc(licenseInfo.valid_until).local()) - ) { - return 'gracePeriod'; - } - - if (licenseInfo.tier === 'Enterprise') { - return 'validEnterprise'; - } - - return 'validBusiness'; -}; - export const SettingsLicenseTab = () => { const { data: licenseInfo } = useQuery(getLicenseInfoQueryOptions); const { data: settings } = useQuery(getSettingsQueryOptions); diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index e850c65e80..0a56a47519 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -16,7 +16,7 @@ import { ProgressionBar } from '../../../../../../../shared/defguard-ui/componen import { SizedBox } from '../../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../../../shared/defguard-ui/types'; import { isPresent } from '../../../../../../../shared/defguard-ui/utils/isPresent'; -import type { LicenseState } from '../../SettingsLicenseTab'; +import type { LicenseState } from '../../../../../../../shared/utils/license'; type Props = { licenseInfo: LicenseInfo; diff --git a/web/src/shared/components/PageTopBar/components/TopBarLicenseExpiration/TopBarLicenseExpiration.tsx b/web/src/shared/components/PageTopBar/components/TopBarLicenseExpiration/TopBarLicenseExpiration.tsx index 158ec90e69..c3c6c0c1f3 100644 --- a/web/src/shared/components/PageTopBar/components/TopBarLicenseExpiration/TopBarLicenseExpiration.tsx +++ b/web/src/shared/components/PageTopBar/components/TopBarLicenseExpiration/TopBarLicenseExpiration.tsx @@ -7,6 +7,7 @@ import { Icon, IconKind } from '../../../../defguard-ui/components/Icon'; import { ThemeVariable } from '../../../../defguard-ui/types'; import { isPresent } from '../../../../defguard-ui/utils/isPresent'; import { getLicenseInfoQueryOptions } from '../../../../query'; +import { getLicenseState } from '../../../../utils/license'; import { TopBarElementSkeleton } from '../../TopBarElementSkeleton'; export const TopBarLicenseExpiration = () => { @@ -21,16 +22,9 @@ type MessageVariant = 'warning' | 'expired' | 'critical' | 'safe'; const Content = () => { const { data: license } = useSuspenseQuery(getLicenseInfoQueryOptions); + const licenseState = getLicenseState(license); - const isGracePeriod = useMemo(() => { - if (!license) return false; - const current = dayjs(); - const expires = dayjs(license.valid_until); - if (current.isAfter(expires) && !license.expired) { - return true; - } - return false; - }, [license]); + const isGracePeriod = licenseState === 'gracePeriod'; const expiresDisplay = useMemo(() => { if (license === null || license.valid_until === null) return ''; @@ -48,20 +42,18 @@ const Content = () => { const variant = useMemo((): MessageVariant => { if (!isPresent(license) || license.valid_until === null || daysToEnd === null) return 'safe'; + if (isGracePeriod) { + return 'critical'; + } + if (licenseState === 'expiredLicense') return 'expired'; if (license.subscription) { - if (isGracePeriod) { - return 'critical'; - } - if (!license.expired) { - return 'safe'; - } + return 'safe'; } - if (license.expired) return 'expired'; if (daysToEnd > 14) return 'safe'; if (daysToEnd <= 14 && daysToEnd > 7) return 'warning'; if (daysToEnd <= 7) return 'critical'; return 'expired'; - }, [daysToEnd, license, isGracePeriod]); + }, [daysToEnd, isGracePeriod, license, licenseState]); if (!isPresent(license) || daysToEnd === null || variant === 'safe') return null; return ( diff --git a/web/src/shared/utils/license.ts b/web/src/shared/utils/license.ts index 7c4aac3c0d..525bdd146c 100644 --- a/web/src/shared/utils/license.ts +++ b/web/src/shared/utils/license.ts @@ -1,13 +1,51 @@ +import dayjs from 'dayjs'; import type { LicenseInfo } from '../api/types'; import { openModal } from '../hooks/modalControls/modalsSubjects'; import { ModalName } from '../hooks/modalControls/modalTypes'; +export type LicenseState = + | 'noLicense' + | 'gracePeriod' + | 'expiredLicense' + | 'validBusiness' + | 'validEnterprise'; + interface LicenseCheckResult { result: boolean; error: 'expired' | 'tier' | null; tierCheck: 'Business' | 'Enterprise'; } +export const getLicenseState = ( + licenseInfo: LicenseInfo | null | undefined, +): LicenseState | null => { + if (licenseInfo === undefined) { + return null; + } + + if (licenseInfo === null) { + return 'noLicense'; + } + + if (licenseInfo.expired) { + return 'expiredLicense'; + } + + if ( + licenseInfo.subscription && + licenseInfo.valid_until !== null && + dayjs().isAfter(dayjs.utc(licenseInfo.valid_until).local()) + ) { + return 'gracePeriod'; + } + + if (licenseInfo.tier === 'Enterprise') { + return 'validEnterprise'; + } + + return 'validBusiness'; +}; + export const licenseActionCheck = ( checkResult: LicenseCheckResult, successCallback: () => void, From d82916e250db29dca2b8808ff0f314fef3eecee9 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 10:00:18 +0100 Subject: [PATCH 09/21] common license expired message --- web/messages/en/settings.json | 2 +- .../SettingsLicenseExpiredNotice.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index f1422f4a34..a098116373 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -69,7 +69,7 @@ "settings_license_try_business_button": "Try Business for free now", "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_grace_period_ended": "Your grace period has ended and Defguard will downgrade to the Open Source version. Please renew your license as soon as possible to restore access to the features included in your previous plan.", + "settings_license_expired_notice_description": "Your license has expired.", "settings_license_expired_notice_button": "Update your license now", "settings_license_unknown": "Unknown", "settings_license_type_title": "License type", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx index 32a4269ef1..c0f599f849 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx @@ -27,7 +27,7 @@ export const SettingsLicenseExpiredNotice = ({ licenseInfo, state }: Props) => { const description = state === 'expiredLicense' - ? m.settings_license_expired_notice_description_grace_period_ended() + ? m.settings_license_expired_notice_description() : m.settings_license_expired_notice_description_grace_period({ duration: remainingDuration, }); From 7d44af5323361a59e5cb8ddb7f7b411085c0dcbf Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 10:28:25 +0100 Subject: [PATCH 10/21] display badge when license expired --- web/messages/en/settings.json | 1 + .../SettingsLicenseInfoSection.tsx | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index a098116373..718606d4da 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -67,6 +67,7 @@ "settings_license_plan_enterprise_title": "Enterprise", "settings_license_plan_enterprise_description": "Custom integrations, and dedicated support tailored to your organization's security and scalability needs.", "settings_license_try_business_button": "Try Business for free now", + "settings_license_grace_period_banner": "Your license key has expired. Please renew your license to continue using Defguard {tier} features.", "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.", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index 0a56a47519..6210dd503b 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -12,6 +12,7 @@ import { Icon, type IconKindValue, } from '../../../../../../../shared/defguard-ui/components/Icon'; +import { InfoBanner } from '../../../../../../../shared/defguard-ui/components/InfoBanner/InfoBanner'; import { ProgressionBar } from '../../../../../../../shared/defguard-ui/components/ProgressionBar/ProgressionBar'; import { SizedBox } from '../../../../../../../shared/defguard-ui/components/SizedBox/SizedBox'; import { ThemeSpacing } from '../../../../../../../shared/defguard-ui/types'; @@ -64,8 +65,18 @@ export const SettingsLicenseInfoSection = ({ )}
+ {isExpired && ( + <> + + + + )} - {isPresent(license.limits) && ( + {!isExpired && isPresent(license.limits) && (

{m.settings_license_limits_title()}

From cc5d3cf7201ec7eadbbc196e115ec131351e59d0 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 10:39:54 +0100 Subject: [PATCH 11/21] fix expired label --- web/messages/en/common.json | 1 - .../SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 0934dc0f19..48c305b9a2 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -10,7 +10,6 @@ "misc_secret": "Secret", "misc_active": "Active", "misc_disabled": "Disabled", - "state_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.", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index 6210dd503b..543912968c 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -39,7 +39,7 @@ export const SettingsLicenseInfoSection = ({ {isPresent(licenseTier) && ( <>

{licenseTier}

- {isExpired && } + {isExpired && } {!isExpired && } )} From 0dd390c44d89204c8471ef3970b7880f961cb87d Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 20 Mar 2026 08:10:03 +0100 Subject: [PATCH 12/21] update translations --- web/messages/en/settings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 718606d4da..ec61c17c52 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -62,15 +62,15 @@ "settings_license_select_plan": "Select your plan", "settings_license_plan_business_title": "Business", "settings_license_plan_business_badge": "Most popular", - "settings_license_plan_business_description": "Advanced protection, shared access controls, and centralized billing - ideal for small to medium teams.", + "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": "Custom integrations, and dedicated support tailored to your organization's security and scalability needs.", + "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_grace_period_banner": "Your license key has expired. Please renew your license to continue using Defguard {tier} features.", "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.", + "settings_license_expired_notice_description": "Your license has expired. Please renew your license to continue using Defguard premium features.", "settings_license_expired_notice_button": "Update your license now", "settings_license_unknown": "Unknown", "settings_license_type_title": "License type", From 260c806bbe232b5292e32178fe1901e2e23450fc Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 20 Mar 2026 08:41:00 +0100 Subject: [PATCH 13/21] display limits if in grace period --- web/messages/en/common.json | 1 + web/messages/en/settings.json | 4 ++-- .../SettingsLicenseExpiredNotice.tsx | 2 +- .../SettingsLicenseInfoSection.tsx | 13 +++++++------ web/src/shared/constants.ts | 2 +- web/src/shared/utils/license.ts | 2 ++ 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 48c305b9a2..53cee842db 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -10,6 +10,7 @@ "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.", diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index ec61c17c52..ef179e315e 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -67,10 +67,10 @@ "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_grace_period_banner": "Your license key has expired. Please renew your license to continue using Defguard {tier} features.", + "settings_license_expired_banner": "Your license key has expired. Please renew your license to continue using Defguard {tier} features.", "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 premium features.", + "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", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx index c0f599f849..b1577fb08f 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx @@ -27,7 +27,7 @@ export const SettingsLicenseExpiredNotice = ({ licenseInfo, state }: Props) => { const description = state === 'expiredLicense' - ? m.settings_license_expired_notice_description() + ? m.settings_license_expired_notice_description({tier: licenseInfo.tier}) : m.settings_license_expired_notice_description_grace_period({ duration: remainingDuration, }); diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index 543912968c..5b5caf8aa6 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -29,9 +29,9 @@ export const SettingsLicenseInfoSection = ({ licenseState, }: Props) => { const licenseTier = license.tier; - // In the settings UI, grace period should use the expired badge. - const isExpired = licenseState === 'gracePeriod' || licenseState === 'expiredLicense'; - + const isGracePeriod = licenseState === 'gracePeriod'; + const isExpired = licenseState === 'expiredLicense'; + const isValid = licenseState === 'validBusiness' || licenseState === 'validEnterprise'; return (
@@ -39,8 +39,9 @@ export const SettingsLicenseInfoSection = ({ {isPresent(licenseTier) && ( <>

{licenseTier}

- {isExpired && } - {!isExpired && } + {isExpired && } + {isGracePeriod && } + {isValid && } )} {!isPresent(licenseTier) && ( @@ -70,7 +71,7 @@ export const SettingsLicenseInfoSection = ({ diff --git a/web/src/shared/constants.ts b/web/src/shared/constants.ts index 8732b3c6b4..4727625996 100644 --- a/web/src/shared/constants.ts +++ b/web/src/shared/constants.ts @@ -39,4 +39,4 @@ export const googleProviderBaseUrl = 'https://accounts.google.com'; export const jumpcloudProviderBaseUrl = 'https://oauth.id.jumpcloud.com'; -export const licenseGracePeriodDays = 14; +export const licenseGracePeriodDays = 40; diff --git a/web/src/shared/utils/license.ts b/web/src/shared/utils/license.ts index 525bdd146c..c66efff387 100644 --- a/web/src/shared/utils/license.ts +++ b/web/src/shared/utils/license.ts @@ -23,6 +23,8 @@ export const getLicenseState = ( return null; } + return 'gracePeriod'; + if (licenseInfo === null) { return 'noLicense'; } From 214276d0b23aa6a8be1f3523f926e72ed4179833 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 20 Mar 2026 09:09:39 +0100 Subject: [PATCH 14/21] "expires soon" warning --- web/messages/en/settings.json | 1 + .../SettingsLicenseExpiredNotice.tsx | 2 +- .../SettingsLicenseInfoSection.tsx | 24 +++++++++++++++++++ web/src/shared/constants.ts | 2 +- web/src/shared/utils/license.ts | 2 -- 5 files changed, 27 insertions(+), 4 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index ef179e315e..1cb15e3248 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -68,6 +68,7 @@ "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.", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx index b1577fb08f..3a3c3b54be 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx @@ -27,7 +27,7 @@ export const SettingsLicenseExpiredNotice = ({ licenseInfo, state }: Props) => { const description = state === 'expiredLicense' - ? m.settings_license_expired_notice_description({tier: licenseInfo.tier}) + ? m.settings_license_expired_notice_description({ tier: licenseInfo.tier }) : m.settings_license_expired_notice_description_grace_period({ duration: remainingDuration, }); diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx index 5b5caf8aa6..a621b75219 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseInfoSection/SettingsLicenseInfoSection.tsx @@ -32,6 +32,20 @@ export const SettingsLicenseInfoSection = ({ const isGracePeriod = licenseState === 'gracePeriod'; const isExpired = licenseState === 'expiredLicense'; const isValid = licenseState === 'validBusiness' || licenseState === 'validEnterprise'; + const daysUntilExpiration = isPresent(license.valid_until) + ? dayjs + .utc(license.valid_until) + .local() + .startOf('day') + .diff(dayjs().startOf('day'), 'day') + : null; + const isOfflineExpiringSoon = + isValid && + !license.subscription && + daysUntilExpiration !== null && + daysUntilExpiration > 0 && + daysUntilExpiration <= 30; + return (
@@ -76,6 +90,16 @@ export const SettingsLicenseInfoSection = ({ /> )} + {!isExpired && isOfflineExpiringSoon && ( + <> + + + + )} {!isExpired && isPresent(license.limits) && ( diff --git a/web/src/shared/constants.ts b/web/src/shared/constants.ts index 4727625996..8732b3c6b4 100644 --- a/web/src/shared/constants.ts +++ b/web/src/shared/constants.ts @@ -39,4 +39,4 @@ export const googleProviderBaseUrl = 'https://accounts.google.com'; export const jumpcloudProviderBaseUrl = 'https://oauth.id.jumpcloud.com'; -export const licenseGracePeriodDays = 40; +export const licenseGracePeriodDays = 14; diff --git a/web/src/shared/utils/license.ts b/web/src/shared/utils/license.ts index c66efff387..525bdd146c 100644 --- a/web/src/shared/utils/license.ts +++ b/web/src/shared/utils/license.ts @@ -23,8 +23,6 @@ export const getLicenseState = ( return null; } - return 'gracePeriod'; - if (licenseInfo === null) { return 'noLicense'; } From b5fe9eab2d1f0215d1dad578056680474e597307 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 20 Mar 2026 09:16:39 +0100 Subject: [PATCH 15/21] "contact sales" button --- web/messages/en/common.json | 1 + .../SettingsLicenseBusinessUpsellSection.tsx | 15 +++++++++++++++ .../SettingsLicenseNoLicenseSection.tsx | 14 ++++++++++++++ web/src/shared/constants.ts | 1 + 4 files changed, 31 insertions(+) diff --git a/web/messages/en/common.json b/web/messages/en/common.json index 53cee842db..39a26f59d4 100644 --- a/web/messages/en/common.json +++ b/web/messages/en/common.json @@ -22,6 +22,7 @@ "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", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx index dcba441862..6b16e72e95 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx @@ -1,6 +1,7 @@ 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'; @@ -32,6 +33,20 @@ export const SettingsLicenseBusinessUpsellSection = () => {

{m.settings_license_plan_enterprise_description()}

+ +
diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx index 7bffd0bc69..ae45ae1a5c 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx @@ -75,6 +75,20 @@ export const SettingsLicenseNoLicenseSection = () => {

{m.settings_license_plan_enterprise_description()}

+ +
diff --git a/web/src/shared/constants.ts b/web/src/shared/constants.ts index 8732b3c6b4..8d34ab381a 100644 --- a/web/src/shared/constants.ts +++ b/web/src/shared/constants.ts @@ -5,6 +5,7 @@ export const externalLink = { docs: 'https://docs.defguard.net', pricing: 'https://defguard.net/pricing', download: 'https://defguard.net/download', + sales: 'mailto:sales@defguard.net', }, client: { desktop: { From e15d2f25d7cfeea0efcfa5941afd8fc632bbcda7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Fri, 20 Mar 2026 09:23:10 +0100 Subject: [PATCH 16/21] remove "open in new window" icon from buttons --- .../SettingsLicenseBusinessUpsellSection.tsx | 1 - .../SettingsLicenseExpiredNotice.tsx | 1 - .../SettingsLicenseNoLicenseSection.tsx | 2 -- 3 files changed, 4 deletions(-) diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx index 6b16e72e95..349b1b38e4 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx @@ -43,7 +43,6 @@ export const SettingsLicenseBusinessUpsellSection = () => {
diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx index 3a3c3b54be..9a3e1b4c67 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx @@ -49,7 +49,6 @@ export const SettingsLicenseExpiredNotice = ({ licenseInfo, state }: Props) => {
diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx index ae45ae1a5c..ce829c41ad 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseNoLicenseSection/SettingsLicenseNoLicenseSection.tsx @@ -55,7 +55,6 @@ export const SettingsLicenseNoLicenseSection = () => {

{m.settings_license_expired_notice_title()}

-

{m.settings_license_expired_notice_description()}

+

{description}

= new Set([ export const googleProviderBaseUrl = 'https://accounts.google.com'; export const jumpcloudProviderBaseUrl = 'https://oauth.id.jumpcloud.com'; + +export const licenseGracePeriodDays = 40; From 19bedaa28c603e5e854385a4b54dea2b9aa610e7 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 07:51:12 +0100 Subject: [PATCH 05/21] distinguish between subscription and non-subscription licenses --- .../SettingsLicenseTab/SettingsLicenseTab.tsx | 21 ++++++++++++------- .../SettingsLicenseExpiredNotice.tsx | 11 +++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx index 0be63ec417..e920c797d1 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/SettingsLicenseTab.tsx @@ -32,8 +32,9 @@ import { SettingsLicenseNoLicenseSection } from './components/SettingsLicenseNoL import { SettingsLicenseModal } from './modals/SettingsLicenseModal/SettingsLicenseModal'; type LicenseSectionState = - | 'expiredLicense' | 'noLicense' + | 'gracePeriod' + | 'expiredLicense' | 'validBusiness' | 'validEnterprise'; @@ -48,15 +49,16 @@ const getLicenseSectionState = ( return 'noLicense'; } + if (licenseInfo.expired) { + return 'expiredLicense'; + } + if ( + licenseInfo.subscription && licenseInfo.valid_until !== null && dayjs().isAfter(dayjs.utc(licenseInfo.valid_until).local()) ) { - return 'expiredLicense'; - } - - if (licenseInfo.expired) { - return 'expiredLicense'; + return 'gracePeriod'; } if (licenseInfo.tier === 'Enterprise') { @@ -136,10 +138,13 @@ const LicenseSection = ({ return ( <> + {state === 'noLicense' && } + {state === 'gracePeriod' && isPresent(licenseInfo) && ( + + )} {state === 'expiredLicense' && isPresent(licenseInfo) && ( - + )} - {state === 'noLicense' && } {state === 'validBusiness' && } ); diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx index 25ac6206c6..32a4269ef1 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseExpiredNotice/SettingsLicenseExpiredNotice.tsx @@ -11,9 +11,10 @@ import expiredImage from '../../assets/expired.png'; type Props = { licenseInfo: LicenseInfo; + state: 'gracePeriod' | 'expiredLicense'; }; -export const SettingsLicenseExpiredNotice = ({ licenseInfo }: Props) => { +export const SettingsLicenseExpiredNotice = ({ licenseInfo, state }: Props) => { const gracePeriodEndsAt = licenseInfo.valid_until ? dayjs.utc(licenseInfo.valid_until).local().add(licenseGracePeriodDays, 'day') : null; @@ -25,11 +26,11 @@ export const SettingsLicenseExpiredNotice = ({ licenseInfo }: Props) => { const remainingDuration = m.settings_duration_days({ days: gracePeriodDaysLeft }); const description = - gracePeriodDaysLeft > 0 - ? m.settings_license_expired_notice_description_grace_period({ + state === 'expiredLicense' + ? m.settings_license_expired_notice_description_grace_period_ended() + : m.settings_license_expired_notice_description_grace_period({ duration: remainingDuration, - }) - : m.settings_license_expired_notice_description_grace_period_ended(); + }); return ( From 62334d225f320775bc0f4783699310be794a3c10 Mon Sep 17 00:00:00 2001 From: Jacek Chmielewski Date: Thu, 19 Mar 2026 08:53:31 +0100 Subject: [PATCH 06/21] business, enterprise translations --- web/messages/en/settings.json | 2 ++ .../SettingsLicenseBusinessUpsellSection.tsx | 2 +- .../SettingsLicenseNoLicenseSection.tsx | 4 ++-- web/src/shared/constants.ts | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/web/messages/en/settings.json b/web/messages/en/settings.json index 79c844af70..f1422f4a34 100644 --- a/web/messages/en/settings.json +++ b/web/messages/en/settings.json @@ -60,9 +60,11 @@ "settings_license_choose_plan_title": "Choose plan that matches your needs", "settings_license_expand_plan_title": "Expand your possibilities with advanced plans", "settings_license_select_plan": "Select your plan", + "settings_license_plan_business_title": "Business", "settings_license_plan_business_badge": "Most popular", "settings_license_plan_business_description": "Advanced protection, shared access controls, and centralized billing - ideal for small to medium teams.", "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": "Custom integrations, and dedicated support tailored to your organization's security and scalability needs.", "settings_license_try_business_button": "Try Business for free now", "settings_license_expired_notice_title": "License expiration notice", diff --git a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx index af26c9b62b..dcba441862 100644 --- a/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx +++ b/web/src/pages/settings/SettingsIndexPage/tabs/SettingsLicenseTab/components/SettingsLicenseBusinessUpsellSection/SettingsLicenseBusinessUpsellSection.tsx @@ -27,7 +27,7 @@ export const SettingsLicenseBusinessUpsellSection = () => {
@@ -85,7 +84,6 @@ export const SettingsLicenseNoLicenseSection = () => {