diff --git a/server/channels/api4/user.go b/server/channels/api4/user.go
index e067dec0cd..e887898872 100644
--- a/server/channels/api4/user.go
+++ b/server/channels/api4/user.go
@@ -2040,6 +2040,12 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
"focalboard": "/boards",
}
+ useCaseToURL := map[string]string{
+ "mission-ops": "/mission-ops-hq",
+ "dev-sec-ops": "/dev-sec-ops-hq",
+ "cyber-defense": "/cyber-defense-hq",
+ }
+
if !c.App.Channels().License().IsCloud() {
c.Err = model.NewAppError("loginCWS", "api.user.login_cws.license.error", nil, "", http.StatusUnauthorized)
return
@@ -2050,6 +2056,7 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
var loginID string
var token string
var campaign string
+ var useCase string
if len(r.Form) > 0 {
for key, value := range r.Form {
if key == "login_id" {
@@ -2061,6 +2068,9 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
if key == "utm_campaign" {
campaign = value[0]
}
+ if key == "use_case" {
+ useCase = value[0]
+ }
}
}
@@ -2099,6 +2109,13 @@ func loginCWS(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
+ // If a cloud preview, redirect to the correct use case URL
+ if c.App.License().IsCloudPreview() && useCase != "" {
+ if url, ok := useCaseToURL[useCase]; ok {
+ redirectURL += url
+ }
+ }
+
http.Redirect(w, r, redirectURL, http.StatusFound)
}
diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx
index e3b9e3a673..3521dcfebb 100644
--- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx
+++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx
@@ -88,4 +88,25 @@ describe('NotificationPermissionBar', () => {
expect(container).toBeEmptyDOMElement();
});
+
+ test('should not render anything in cloud preview environment', () => {
+ jest.spyOn(utilsNotifications, 'isNotificationAPISupported').mockReturnValue(true);
+ jest.spyOn(utilsNotifications, 'getNotificationPermission').mockReturnValue(utilsNotifications.NotificationPermissionNeverGranted);
+
+ const stateWithCloudPreview = {
+ ...initialState,
+ entities: {
+ ...initialState.entities,
+ cloud: {
+ subscription: {
+ is_cloud_preview: true,
+ },
+ },
+ },
+ };
+
+ const {container} = renderWithContext(, stateWithCloudPreview);
+
+ expect(container).toBeEmptyDOMElement();
+ });
});
diff --git a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx
index 17304e6f0b..8547a77840 100644
--- a/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx
+++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.tsx
@@ -4,6 +4,7 @@
import React from 'react';
import {useSelector} from 'react-redux';
+import {getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import NotificationPermissionNeverGrantedBar from 'components/announcement_bar/notification_permission_bar/notification_permission_never_granted_bar';
@@ -19,6 +20,7 @@ import {
export default function NotificationPermissionBar() {
const isLoggedIn = Boolean(useSelector(getCurrentUserId));
+ const subscription = useSelector(getCloudSubscription);
useDesktopAppNotificationPermission();
@@ -26,6 +28,11 @@ export default function NotificationPermissionBar() {
return null;
}
+ // Don't show the notification bar if it's a cloud preview environment
+ if (subscription?.is_cloud_preview) {
+ return null;
+ }
+
// When browser does not support notification API, we show the notification bar to update browser
if (!isNotificationAPISupported()) {
return ;
diff --git a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx
index cbdd62a61a..89f0780597 100644
--- a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx
+++ b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.test.tsx
@@ -6,6 +6,7 @@ import React from 'react';
import * as reactRedux from 'react-redux';
import type {Subscription} from '@mattermost/types/cloud';
+import type {TeamType} from '@mattermost/types/teams';
import {renderWithContext} from 'tests/react_testing_utils';
@@ -66,6 +67,30 @@ describe('CloudPreviewModal', () => {
current_user_id: {roles: 'system_admin'},
},
},
+ teams: {
+ currentTeamId: 'mission-ops-hq',
+ teams: {
+ 'mission-ops-hq': {
+ id: 'mission-ops-hq',
+ name: 'mission-ops-hq',
+ display_name: 'Mission Ops HQ',
+ type: 'O' as TeamType,
+ create_at: 1234567890,
+ update_at: 1234567890,
+ delete_at: 0,
+ allow_open_invite: true,
+ invite_id: 'test-invite-id',
+ description: 'Test team',
+ email: 'test@example.com',
+ company_name: 'Test Company',
+ allowed_domains: '',
+ scheme_id: '',
+ group_constrained: false,
+ policy_id: null,
+ cloud_limits_archived: false,
+ },
+ },
+ },
cloud: {
subscription: baseSubscription,
},
@@ -122,27 +147,26 @@ describe('CloudPreviewModal', () => {
expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
});
- // Skip for now.
- // it('should not show modal when modal has been shown before', () => {
- // const state = JSON.parse(JSON.stringify(initialState));
- // state.entities.preferences.myPreferences = {
- // 'cloud_preview_modal_shown--cloud_preview_modal_shown': {
- // category: 'cloud_preview_modal_shown',
- // name: 'cloud_preview_modal_shown',
- // value: 'true',
- // },
- // };
+ it('should not show modal when modal has been shown before', () => {
+ const state = JSON.parse(JSON.stringify(initialState));
+ state.entities.preferences.myPreferences = {
+ 'cloud_preview_modal_shown--cloud_preview_modal_shown': {
+ category: 'cloud_preview_modal_shown',
+ name: 'cloud_preview_modal_shown',
+ value: 'true',
+ },
+ };
- // const dummyDispatch = jest.fn();
- // useDispatchMock.mockReturnValue(dummyDispatch);
+ const dummyDispatch = jest.fn();
+ useDispatchMock.mockReturnValue(dummyDispatch);
- // renderWithContext(
- // ,
- // state,
- // );
+ renderWithContext(
+ ,
+ state,
+ );
- // expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
- // });
+ expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
+ });
it('should save preference when modal is closed', async () => {
const dummyDispatch = jest.fn();
@@ -180,4 +204,69 @@ describe('CloudPreviewModal', () => {
expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
});
+
+ it('should not show modal when no team is present', () => {
+ const state = JSON.parse(JSON.stringify(initialState));
+ delete state.entities.teams;
+
+ const dummyDispatch = jest.fn();
+ useDispatchMock.mockReturnValue(dummyDispatch);
+
+ renderWithContext(
+ ,
+ state,
+ );
+
+ expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
+ });
+
+ it('should filter content based on team use case', () => {
+ const dummyDispatch = jest.fn();
+ useDispatchMock.mockReturnValue(dummyDispatch);
+
+ // Test with dev-sec-ops team
+ const devSecOpsState = JSON.parse(JSON.stringify(initialState));
+ devSecOpsState.entities.teams.currentTeamId = 'dev-sec-ops-hq';
+ devSecOpsState.entities.teams.teams = {
+ 'dev-sec-ops-hq': {
+ ...initialState.entities.teams.teams['mission-ops-hq'],
+ id: 'dev-sec-ops-hq',
+ name: 'dev-sec-ops-hq',
+ display_name: 'DevSecOps HQ',
+ },
+ };
+
+ renderWithContext(
+ ,
+ devSecOpsState,
+ );
+
+ // Should show modal for dev-sec-ops team
+ expect(screen.getByTestId('preview-modal-controller')).toBeInTheDocument();
+ });
+
+ it('should not show modal when team name does not match any use case', () => {
+ const dummyDispatch = jest.fn();
+ useDispatchMock.mockReturnValue(dummyDispatch);
+
+ // Test with unknown team
+ const unknownTeamState = JSON.parse(JSON.stringify(initialState));
+ unknownTeamState.entities.teams.currentTeamId = 'unknown-team-hq';
+ unknownTeamState.entities.teams.teams = {
+ 'unknown-team-hq': {
+ ...initialState.entities.teams.teams['mission-ops-hq'],
+ id: 'unknown-team-hq',
+ name: 'unknown-team-hq',
+ display_name: 'Unknown Team HQ',
+ },
+ };
+
+ renderWithContext(
+ ,
+ unknownTeamState,
+ );
+
+ // Should not show modal for unknown team
+ expect(screen.queryByTestId('preview-modal-controller')).not.toBeInTheDocument();
+ });
});
diff --git a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx
index 7fbfb1367e..4380f2694f 100644
--- a/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx
+++ b/webapp/channels/src/components/cloud_preview_modal/cloud_preview_modal_controller.tsx
@@ -7,10 +7,17 @@ import {useSelector, useDispatch} from 'react-redux';
import {savePreferences} from 'mattermost-redux/actions/preferences';
import {getCloudSubscription} from 'mattermost-redux/selectors/entities/cloud';
import {getLicense} from 'mattermost-redux/selectors/entities/general';
+import {getBool} from 'mattermost-redux/selectors/entities/preferences';
+import {getCurrentTeam} from 'mattermost-redux/selectors/entities/teams';
import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import {makeAsyncComponent} from 'components/async_load';
+import type {GlobalState} from 'types/store';
+
+import type {PreviewModalContentData} from './preview_modal_content_data';
+import {modalContent} from './preview_modal_content_data';
+
const CLOUD_PREVIEW_MODAL_SHOWN_PREF = 'cloud_preview_modal_shown';
// Lazy load the controller component and content data together
@@ -24,26 +31,33 @@ const CloudPreviewModal: React.FC = () => {
const subscription = useSelector(getCloudSubscription);
const license = useSelector(getLicense);
const currentUserId = useSelector(getCurrentUserId);
+ const team = useSelector(getCurrentTeam);
const isCloud = license?.Cloud === 'true';
const isCloudPreview = subscription?.is_cloud_preview === true;
// Check if modal has been shown before
- const hasModalBeenShown = false;
+ const hasModalBeenShown = useSelector((state: GlobalState) => getBool(state, CLOUD_PREVIEW_MODAL_SHOWN_PREF, CLOUD_PREVIEW_MODAL_SHOWN_PREF));
const [showModal, setShowModal] = useState(false);
+ const filteredContentByUseCase = (content: PreviewModalContentData[]) => {
+ return content.filter((content) => content.useCase === team?.name.replace('-hq', ''));
+ };
+
useEffect(() => {
// Show modal only if:
// 1. It's a cloud preview environment
// 2. Modal hasn't been shown before
// 3. We have the necessary data loaded
- if (isCloud && isCloudPreview && !hasModalBeenShown && currentUserId) {
+ // 4. There's content to display for the current team
+ const filteredContent = team?.name ? filteredContentByUseCase(modalContent) : [];
+ if (isCloud && isCloudPreview && !hasModalBeenShown && currentUserId && team?.name && filteredContent.length > 0) {
setShowModal(true);
} else if (hasModalBeenShown) {
setShowModal(false);
}
- }, [isCloud, isCloudPreview, hasModalBeenShown, currentUserId]);
+ }, [isCloud, isCloudPreview, hasModalBeenShown, currentUserId, team?.name]);
const handleClose = () => {
setShowModal(false);
@@ -66,10 +80,13 @@ const CloudPreviewModal: React.FC = () => {
}
// Only render the controller if we pass the license checks
+ const contentData = team?.name ? filteredContentByUseCase(modalContent) : [];
+
return (
);
};
diff --git a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx
index 19d46ff02e..88f31c5d53 100644
--- a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx
+++ b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_controller.tsx
@@ -24,8 +24,8 @@ const PreviewModalController: React.FC = ({show, onClose, contentData}) =
const [currentIndex, setCurrentIndex] = useState(0);
// Use provided contentData or default to filtered modalContent
- // TODO: Remove hard coded filter on missionops in favour of dynamic based on selected use case
- const activeContentData = contentData || modalContent.filter((content) => content.useCase === 'missionops');
+ // Use passed contentData if it's provided otherwise use the default mission ops content
+ const activeContentData = contentData || modalContent.filter((content) => content.useCase === 'mission-ops');
const handlePrevious = useCallback(() => {
setCurrentIndex((prev) => Math.max(0, prev - 1));
diff --git a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts
index 0341cdbb4f..e13faff82f 100644
--- a/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts
+++ b/webapp/channels/src/components/cloud_preview_modal/preview_modal_content_data.ts
@@ -29,7 +29,7 @@ export const modalContent: PreviewModalContentData[] = [
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/Mattermost_TMM_Demo_Mission+Ops_20250307.mp4',
videoPoster: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/Mattermost_TMM_Demo_MissionOps_Poster.jpg',
- useCase: 'missionops',
+ useCase: 'mission-ops',
},
{
skuLabel: defineMessage({
@@ -45,7 +45,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/priority-messages.jpg',
- useCase: 'missionops',
+ useCase: 'mission-ops',
},
{
skuLabel: defineMessage({
@@ -61,7 +61,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/ai-search.jpg',
- useCase: 'missionops',
+ useCase: 'mission-ops',
},
{
skuLabel: defineMessage({
@@ -77,7 +77,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization\'s structure. Help teams understand who they\'re working with and how to collaborate more effectively.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/custom-profile-attributes.jpg',
- useCase: 'missionops',
+ useCase: 'mission-ops',
},
{
skuLabel: defineMessage({
@@ -93,7 +93,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/playbook-properties.jpg',
- useCase: 'missionops',
+ useCase: 'mission-ops',
},
// {
@@ -126,7 +126,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/missionops/modal-assets/zero-trust.jpg',
- useCase: 'missionops',
+ useCase: 'mission-ops',
},
{
skuLabel: defineMessage({
@@ -143,7 +143,7 @@ export const modalContent: PreviewModalContentData[] = [
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/Mattermost_TMM_Demo_DevSecOps_20260610.mp4',
videoPoster: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/Mattermost_TMM_Demo_DevSecOps_Poster.jpg',
- useCase: 'devsecops',
+ useCase: 'dev-sec-ops',
},
{
skuLabel: defineMessage({
@@ -159,7 +159,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/priority-messages.jpg',
- useCase: 'devsecops',
+ useCase: 'dev-sec-ops',
},
{
skuLabel: defineMessage({
@@ -175,7 +175,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/ai-search.jpg',
- useCase: 'devsecops',
+ useCase: 'dev-sec-ops',
},
{
skuLabel: defineMessage({
@@ -191,7 +191,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization\'s structure. Help teams understand who they\'re working with and how to collaborate more effectively.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/custom-profile-attributes.jpg',
- useCase: 'devsecops',
+ useCase: 'dev-sec-ops',
},
{
skuLabel: defineMessage({
@@ -207,7 +207,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/playbook-properties.jpg',
- useCase: 'devsecops',
+ useCase: 'dev-sec-ops',
},
// {
@@ -240,7 +240,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/devsecops/modal-assets/zero-trust.jpg',
- useCase: 'devsecops',
+ useCase: 'dev-sec-ops',
},
{
skuLabel: defineMessage({
@@ -257,7 +257,7 @@ export const modalContent: PreviewModalContentData[] = [
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/Mattermost_TMM_Demo_Cyber_Defense_20250417.mp4',
videoPoster: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/Mattermost_TMM_Demo_Cyber_Defense_Poster.jpg',
- useCase: 'cyberdefense',
+ useCase: 'cyber-defense',
},
{
skuLabel: defineMessage({
@@ -273,7 +273,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Bring conversations and context together in one secure platform. Communicate with urgency using priority levels, persistent notifications, and acknowledgments—so critical messages are seen and acted on when every second counts.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/priority-messages.jpg',
- useCase: 'cyberdefense',
+ useCase: 'cyber-defense',
},
{
skuLabel: defineMessage({
@@ -289,7 +289,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Supercharge collaboration with Agents. Instantly summarize calls, surface action items, and find answers fast—all using the model you trust.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/ai-search.jpg',
- useCase: 'cyberdefense',
+ useCase: 'cyber-defense',
},
{
skuLabel: defineMessage({
@@ -305,7 +305,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Create tailored user profiles with custom attributes like role, location, or clearance level to reflect your organization\'s structure. Help teams understand who they\'re working with and how to collaborate more effectively.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/custom-profile-attributes.jpg',
- useCase: 'cyberdefense',
+ useCase: 'cyber-defense',
},
{
skuLabel: defineMessage({
@@ -321,7 +321,7 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Unlock powerful workflows tailored to real-world complexity. When conditions change, define tasks for the Playbook to evolve with your dynamic processes.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/playbook-properties.jpg',
- useCase: 'cyberdefense',
+ useCase: 'cyber-defense',
},
// {
@@ -354,6 +354,6 @@ export const modalContent: PreviewModalContentData[] = [
defaultMessage: 'Define granular access to content using attribute-based policies, and display classification banners and labels to guide user behavior. Limit exposure based on role, clearance level, or operational context.',
}),
videoUrl: 'https://mattermost-cloud-preview-assets.s3.us-east-2.amazonaws.com/cyberdefense/modal-assets/zero-trust.jpg',
- useCase: 'cyberdefense',
+ useCase: 'cyber-defense',
},
];