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
new file mode 100644
index 0000000000..d534ad0194
--- /dev/null
+++ b/webapp/channels/src/components/announcement_bar/notification_permission_bar/index.test.tsx
@@ -0,0 +1,78 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {screen, waitFor} from '@testing-library/react';
+import React from 'react';
+
+import {renderWithContext, userEvent} from 'tests/react_testing_utils';
+import {requestNotificationPermission, isNotificationAPISupported} from 'utils/notifications';
+
+import NotificationPermissionBar from './index';
+
+jest.mock('utils/notifications', () => ({
+ requestNotificationPermission: jest.fn(),
+ isNotificationAPISupported: jest.fn(),
+}));
+
+describe('NotificationPermissionBar', () => {
+ const initialState = {
+ entities: {
+ users: {
+ currentUserId: 'user-id',
+ },
+ general: {
+ config: {},
+ license: {},
+ },
+ },
+ };
+
+ beforeEach(() => {
+ (isNotificationAPISupported as jest.Mock).mockReturnValue(true);
+ (window as any).Notification = {permission: 'default'};
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ delete (window as any).Notification;
+ });
+
+ test('should render the notification bar when conditions are met', () => {
+ renderWithContext(, initialState);
+
+ expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument();
+ expect(screen.getByText('Enable notifications')).toBeInTheDocument();
+ });
+
+ test('should not render the notification bar if user is not logged in', () => {
+ renderWithContext();
+
+ expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument();
+ expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument();
+ });
+
+ test('should not render the notification bar if Notifications are not supported', () => {
+ delete (window as any).Notification;
+ (isNotificationAPISupported as jest.Mock).mockReturnValue(false);
+
+ renderWithContext(, initialState);
+
+ expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument();
+ expect(screen.queryByText('Enable notifications')).not.toBeInTheDocument();
+ });
+
+ test('should call requestNotificationPermission and hide the bar when the button is clicked', async () => {
+ (requestNotificationPermission as jest.Mock).mockResolvedValue('granted');
+
+ renderWithContext(, initialState);
+
+ expect(screen.getByText('We need your permission to show desktop notifications.')).toBeInTheDocument();
+
+ await waitFor(async () => {
+ userEvent.click(screen.getByText('Enable notifications'));
+ });
+
+ expect(requestNotificationPermission).toHaveBeenCalled();
+ expect(screen.queryByText('We need your permission to show desktop notifications.')).not.toBeInTheDocument();
+ });
+});
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 9823eaed80..e4e0c130fd 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
@@ -10,25 +10,25 @@ import {getCurrentUserId} from 'mattermost-redux/selectors/entities/users';
import AnnouncementBar from 'components/announcement_bar/default_announcement_bar';
import {AnnouncementBarTypes} from 'utils/constants';
+import {requestNotificationPermission, isNotificationAPISupported} from 'utils/notifications';
export default function NotificationPermissionBar() {
const isLoggedIn = Boolean(useSelector(getCurrentUserId));
- const [show, setShow] = useState(Notification.permission === 'default');
+ const [show, setShow] = useState(isNotificationAPISupported() ? Notification.permission === 'default' : false);
- const handleClose = useCallback(() => {
- // If the user closes the bar, don't show the notification bar any more for the rest of the session, but
- // show it again after refresh.
+ const handleClick = useCallback(async () => {
+ await requestNotificationPermission();
setShow(false);
}, []);
- const handleClick = useCallback(() => {
- Notification.requestPermission().then(() => {
- setShow(false);
- });
+ const handleClose = useCallback(() => {
+ // If the user closes the bar, don't show the notification bar any more for the rest of the session, but
+ // show it again on app refresh.
+ setShow(false);
}, []);
- if (!show || !isLoggedIn) {
+ if (!show || !isLoggedIn || !isNotificationAPISupported()) {
return null;
}
@@ -39,7 +39,7 @@ export default function NotificationPermissionBar() {
type={AnnouncementBarTypes.ANNOUNCEMENT}
message={
}
diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json
index 1a819388f9..2ed2f5313e 100644
--- a/webapp/channels/src/i18n/en.json
+++ b/webapp/channels/src/i18n/en.json
@@ -2814,7 +2814,7 @@
"announcement_bar.error.trial_license_expiring_last_day.short": "This is the last day of your free trial.",
"announcement_bar.notification.email_verified": "Email verified",
"announcement_bar.notification.enable_notifications": "Enable notifications",
- "announcement_bar.notification.needs_permisson": "We need your permission to show desktop notifications.",
+ "announcement_bar.notification.needs_permission": "We need your permission to show desktop notifications.",
"announcement_bar.warn.contact_support_text": "To renew your license, contact support at support@mattermost.com.",
"announcement_bar.warn.email_support": "[Contact support](!{email}).",
"announcement_bar.warn.no_internet_connection": "Looks like you do not have access to the internet.",
diff --git a/webapp/channels/src/utils/notifications.test.ts b/webapp/channels/src/utils/notifications.test.ts
index 306232b095..7228db7729 100644
--- a/webapp/channels/src/utils/notifications.test.ts
+++ b/webapp/channels/src/utils/notifications.test.ts
@@ -3,11 +3,10 @@
/* eslint-disable global-require */
-// to enable being a typescript file
-export const a = '';
import configureStore from 'tests/test_store';
import type {showNotification} from './notifications';
+import {isNotificationAPISupported, requestNotificationPermission} from './notifications';
declare global {
interface Window {
@@ -39,20 +38,20 @@ describe('Notifications.showNotification', () => {
it('should throw an exception if Notification is not defined on window', async () => {
delete window.Notification;
- await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification not supported');
+ await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification API is not supported');
});
it('should throw an exception if Notification.requestPermission is not defined', async () => {
window.Notification = jest.fn();
- await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification.requestPermission not supported');
+ await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification API is not supported');
});
it('should throw an exception if Notification.requestPermission is not a function', async () => {
window.Notification = jest.fn();
window.Notification.requestPermission = true;
- await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification.requestPermission not supported');
+ await expect(store.dispatch(Notifications.showNotification())).rejects.toThrow('Notification API is not supported');
expect(window.Notification).not.toHaveBeenCalled();
});
@@ -241,3 +240,64 @@ describe('Notifications.showNotification', () => {
expect(window.Notification.requestPermission).toHaveBeenCalledTimes(0);
});
});
+
+describe('Notifications.isNotificationAPISupported', () => {
+ beforeEach(() => {
+ window.Notification = {
+ requestPermission: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ delete (window as any).Notification;
+ });
+
+ it('should return true if Notification is supported', () => {
+ expect(isNotificationAPISupported()).toBe(true);
+ });
+
+ it('should return false if Notification is not supported', () => {
+ delete (window as any).Notification;
+
+ expect(isNotificationAPISupported()).toBe(false);
+ });
+
+ it('should return false if requestPermission is not a function', () => {
+ (window as any).Notification = {};
+
+ expect(isNotificationAPISupported()).toBe(false);
+ });
+});
+
+describe('Notifications.requestNotificationPermission', () => {
+ beforeEach(() => {
+ (window as any).Notification = {
+ requestPermission: jest.fn(),
+ };
+ });
+
+ afterEach(() => {
+ delete (window as any).Notification;
+ });
+
+ it('should return the permission if Notification.requestPermission resolves', async () => {
+ (window as any).Notification.requestPermission = jest.fn().mockResolvedValue('granted');
+
+ const permission = await requestNotificationPermission();
+ expect(permission).toBe('granted');
+ });
+
+ it('should return null if Notification is not supported', async () => {
+ delete (window as any).Notification;
+
+ const permission = await requestNotificationPermission();
+ expect(permission).toBeNull();
+ });
+
+ it('should return null if requestPermission throws an error', async () => {
+ (window as any).Notification.requestPermission = jest.fn().mockRejectedValue('some error');
+
+ const permission = await requestNotificationPermission();
+ expect(permission).toBeNull();
+ });
+});
diff --git a/webapp/channels/src/utils/notifications.ts b/webapp/channels/src/utils/notifications.ts
index b01f57a736..15095e1a96 100644
--- a/webapp/channels/src/utils/notifications.ts
+++ b/webapp/channels/src/utils/notifications.ts
@@ -50,12 +50,8 @@ export function showNotification(
icon = iconWS;
}
- if (!('Notification' in window)) {
- throw new Error('Notification not supported');
- }
-
- if (typeof Notification.requestPermission !== 'function') {
- throw new Error('Notification.requestPermission not supported');
+ if (!isNotificationAPISupported()) {
+ throw new Error('Notification API is not supported');
}
if (Notification.permission !== 'granted') {
@@ -104,3 +100,20 @@ export function showNotification(
};
};
}
+
+export function isNotificationAPISupported() {
+ return ('Notification' in window) && (typeof Notification.requestPermission === 'function');
+}
+
+export async function requestNotificationPermission() {
+ if (!isNotificationAPISupported()) {
+ return null;
+ }
+
+ try {
+ const notificationPermission = await Notification.requestPermission();
+ return notificationPermission;
+ } catch (error) {
+ return null;
+ }
+}