[MM-70130] Add permanent banner for non-production developer license keys (#37846)

* [MM-70130] Add permanent banner for non-production developer license keys

Co-authored-by: nick.misasi <[email protected]>

* [MM-70130] Update non-production license banner text

Co-authored-by: Nick Misasi <[email protected]>

---------

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
Nick Misasi
2026-08-07 16:44:46 +00:00
committed by GitHub
co-authored by Hanzo Dev
parent 32ca2cb48d
commit 3e6e5ad2f4
11 changed files with 147 additions and 5 deletions
+3
View File
@@ -973,6 +973,9 @@
is_gov_sku:
type: boolean
description: Whether this is a government SKU license
is_non_production:
type: boolean
description: Whether the license is a non-production (developer) key
customer:
type: object
properties:
+1
View File
@@ -412,6 +412,7 @@ func (ps *PlatformService) logLicense(message string, license *model.License) {
mlog.String("sku_short_name", license.SkuShortName),
mlog.Bool("is_trial", license.IsTrial),
mlog.Bool("is_gov_sku", license.IsGovSku),
mlog.Bool("is_non_production", license.IsNonProduction),
)
if license.Customer != nil {
@@ -152,6 +152,7 @@ func (ps *PlatformService) getSupportPacketDiagnostics(rctx request.CTX) (*model
d.License.SkuShortName = license.SkuShortName
d.License.IsTrial = license.IsTrial
d.License.IsGovSKU = license.IsGovSku
d.License.IsNonProduction = license.IsNonProduction
}
/* Server */
+1
View File
@@ -264,6 +264,7 @@ func GetClientLicense(l *model.License) map[string]string {
props["OutgoingOAuthConnections"] = strconv.FormatBool(*l.Features.OutgoingOAuthConnections)
props["IsTrial"] = strconv.FormatBool(l.IsTrial)
props["IsGovSku"] = strconv.FormatBool(l.IsGovSku)
props["IsNonProduction"] = strconv.FormatBool(l.IsNonProduction)
}
return props
+19
View File
@@ -253,6 +253,25 @@ func TestGetLicenseFileLocation(t *testing.T) {
require.Equal(t, fileName, "mattermost.mattermost-license", "invalid file name")
}
func TestGetClientLicense(t *testing.T) {
license := &model.License{
Customer: &model.Customer{},
Features: &model.Features{},
}
license.Features.SetDefaults()
props := GetClientLicense(license)
require.Equal(t, "false", props["IsNonProduction"])
license.IsNonProduction = true
props = GetClientLicense(license)
require.Equal(t, "true", props["IsNonProduction"])
// The flag must survive sanitization so all users can see the non-production banner.
sanitized := GetSanitizedClientLicense(props)
require.Equal(t, "true", sanitized["IsNonProduction"])
}
func TestGetLicenseFileFromDisk(t *testing.T) {
t.Run("missing file", func(t *testing.T) {
fileBytes := GetLicenseFileFromDisk("thisfileshouldnotexist.mattermost-license")
+1
View File
@@ -86,6 +86,7 @@ type License struct {
SkuShortName string `json:"sku_short_name"`
IsTrial bool `json:"is_trial"`
IsGovSku bool `json:"is_gov_sku"`
IsNonProduction bool `json:"is_non_production"`
IsSeatCountEnforced bool `json:"is_seat_count_enforced"`
// ExtraUsers provides a grace mechanism that allows a configurable number of users
// beyond the base license limit before restricting user creation. When nil, defaults to 0.
+6 -5
View File
@@ -14,11 +14,12 @@ type SupportPacketDiagnostics struct {
Version int `yaml:"version"`
License struct {
Company string `yaml:"company"`
Users int `yaml:"users"`
SkuShortName string `yaml:"sku_short_name"`
IsTrial bool `yaml:"is_trial,omitempty"`
IsGovSKU bool `yaml:"is_gov_sku,omitempty"`
Company string `yaml:"company"`
Users int `yaml:"users"`
SkuShortName string `yaml:"sku_short_name"`
IsTrial bool `yaml:"is_trial,omitempty"`
IsGovSKU bool `yaml:"is_gov_sku,omitempty"`
IsNonProduction bool `yaml:"is_non_production,omitempty"`
} `yaml:"license"`
Server struct {
@@ -12,6 +12,7 @@ import CloudTrialAnnouncementBar from './cloud_trial_announcement_bar';
import CloudTrialEndAnnouncementBar from './cloud_trial_ended_announcement_bar';
import ConfigurationAnnouncementBar from './configuration_bar';
import AnnouncementBar from './default_announcement_bar';
import NonProductionLicenseAnnouncementBar from './non_production_license_bar';
import NotificationPermissionBar from './notification_permission_bar';
import OverageUsersBanner from './overage_users_banner';
import PaymentAnnouncementBar from './payment_announcement_bar';
@@ -105,6 +106,8 @@ class AnnouncementBarController extends React.PureComponent<Props> {
// If set with class 'admin-announcement', they will always be visible, stacked vertically.
return (
<>
{/* Lowest priority: urgent bars temporarily override it, and it reappears once they clear. */}
<NonProductionLicenseAnnouncementBar/>
<NotificationPermissionBar/>
{adminConfiguredAnnouncementBar}
{errorBar}
@@ -0,0 +1,38 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {useSelector} from 'react-redux';
import {AlertOutlineIcon} from '@mattermost/compass-icons/components';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
import {AnnouncementBarTypes} from 'utils/constants';
import AnnouncementBar from '../default_announcement_bar';
const NonProductionLicenseAnnouncementBar: React.FC = () => {
const license = useSelector(getLicense);
if (license?.IsNonProduction !== 'true') {
return null;
}
return (
<AnnouncementBar
type={AnnouncementBarTypes.ADVISOR}
showCloseButton={false}
message={
<FormattedMessage
id='announcement_bar.non_production_license.message'
defaultMessage='Non-production license. Test or staging use only.'
/>
}
icon={<AlertOutlineIcon size={16}/>}
/>
);
};
export default NonProductionLicenseAnnouncementBar;
@@ -0,0 +1,73 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {renderWithContext} from 'tests/react_testing_utils';
import NonProductionLicenseAnnouncementBar from './index';
describe('components/announcement_bar/NonProductionLicenseAnnouncementBar', () => {
const initialState = {
entities: {
general: {
license: {
IsLicensed: 'true',
IsNonProduction: 'true',
},
},
},
};
it('should show banner when license is non-production', () => {
const {container} = renderWithContext(
<NonProductionLicenseAnnouncementBar/>,
initialState,
);
expect(container.querySelector('.announcement-bar')).not.toBeNull();
expect(container.textContent).toContain('Non-production license. Test or staging use only.');
});
it('should not show banner when license is not non-production', () => {
const state = JSON.parse(JSON.stringify(initialState));
state.entities.general.license.IsNonProduction = 'false';
const {container} = renderWithContext(
<NonProductionLicenseAnnouncementBar/>,
state,
);
expect(container.querySelector('.announcement-bar')).toBeNull();
});
it('should not show banner when the flag is absent from the license', () => {
const state = JSON.parse(JSON.stringify(initialState));
delete state.entities.general.license.IsNonProduction;
const {container} = renderWithContext(
<NonProductionLicenseAnnouncementBar/>,
state,
);
expect(container.querySelector('.announcement-bar')).toBeNull();
});
it('should not be dismissable', () => {
const {container} = renderWithContext(
<NonProductionLicenseAnnouncementBar/>,
initialState,
);
expect(container.querySelector('.announcement-bar__close')).toBeNull();
});
it('should have advisor type', () => {
const {container} = renderWithContext(
<NonProductionLicenseAnnouncementBar/>,
initialState,
);
expect(container.querySelector('.announcement-bar.announcement-bar-advisor')).not.toBeNull();
});
});
+1
View File
@@ -4027,6 +4027,7 @@
"announcement_bar.error.trial_license_expiring": "There are {days} days left on your free trial.",
"announcement_bar.error.trial_license_expiring_last_day": "This is the last day of your free trial. Purchase a license now to continue using Mattermost Professional and Enterprise features.",
"announcement_bar.error.trial_license_expiring_last_day.short": "This is the last day of your free trial.",
"announcement_bar.non_production_license.message": "Non-production license. Test or staging use only.",
"announcement_bar.notification.email_verified": "Email verified",
"announcement_bar.warn.contact_support_email": "<a>Contact support</a>.",
"announcement_bar.warn.contact_support_text": "To renew your license, contact support at [email protected].",