Biometric prompt, Jailbreak / Root detection and screenshot prevention (#8645)
* Handle biometric authentication * jailbreak/root detection and biometric small fixes * remove server from initializeSecurityManager and fix loginEntry * Add screen capture prevention and other small fixes * added unit tests to SecurityManager * added shielded nativeID to protect views * use MobilePreventScreenCapture instead of MobileAllowScreenshots in config type definition * Apply Swizzle for screen capture on iOS * Apply patch to bottom sheet to prevent screen captures * fix ios sendReply * Fix SDWebImage swizzle to use the correct session * Fix potential crash on Android when using hardware keyboard * rename patch for network library to remove warning * add temp emm reference * fix initializeSecurityManager tests * fix translations * use siteName for jailbreak detection when connecting to a new server * fix i18n typo * do not query the entire config from the db only the required fields * migrate manage_apps to use defineMessages * use TestHelper.wait in tests * use defineMessages for security manager * fix missing else statement for gm_to_channel * created a TestHelper function to mockQuery and replace as jest.Mock with jest.mocked * fix unit tests * fix unit tests (again) and include setting the test environment to UTC * Fix keyboard disappearing on iOS * update react-native-emm
This commit is contained in:
parent
d84a0afd3a
commit
a5a1e53827
75 changed files with 1923 additions and 253 deletions
207
app/actions/app/server.test.ts
Normal file
207
app/actions/app/server.test.ts
Normal file
|
|
@ -0,0 +1,207 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {createIntl} from 'react-intl';
|
||||||
|
import {Navigation} from 'react-native-navigation';
|
||||||
|
|
||||||
|
import {doPing} from '@actions/remote/general';
|
||||||
|
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||||
|
import {Preferences, Screens} from '@constants';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
|
import {getServer, getServerByIdentifier, queryAllActiveServers} from '@queries/app/servers';
|
||||||
|
import {getSecurityConfig} from '@queries/servers/system';
|
||||||
|
import TestHelper from '@test/test_helper';
|
||||||
|
import {logError} from '@utils/log';
|
||||||
|
import {canReceiveNotifications} from '@utils/push_proxy';
|
||||||
|
import {alertServerAlreadyConnected, alertServerError, loginToServer} from '@utils/server';
|
||||||
|
|
||||||
|
import * as Actions from './server';
|
||||||
|
|
||||||
|
import type {ServerDatabase} from '@typings/database/database';
|
||||||
|
import type ServersModel from '@typings/database/models/app/servers';
|
||||||
|
|
||||||
|
jest.mock('@queries/app/servers');
|
||||||
|
jest.mock('@queries/servers/system');
|
||||||
|
jest.mock('@database/manager', () => ({
|
||||||
|
getServerDatabaseAndOperator: jest.fn(),
|
||||||
|
setActiveServerDatabase: jest.fn(),
|
||||||
|
getActiveServerUrl: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('@managers/security_manager');
|
||||||
|
jest.mock('@managers/websocket_manager');
|
||||||
|
jest.mock('@utils/log');
|
||||||
|
jest.mock('@utils/push_proxy');
|
||||||
|
jest.mock('@utils/server');
|
||||||
|
jest.mock('@actions/remote/general');
|
||||||
|
jest.mock('@actions/remote/systems');
|
||||||
|
jest.mock('react-native-navigation');
|
||||||
|
|
||||||
|
const translations = getTranslations(DEFAULT_LOCALE);
|
||||||
|
const intl = createIntl({locale: DEFAULT_LOCALE, messages: translations});
|
||||||
|
const theme = Preferences.THEMES.denim;
|
||||||
|
|
||||||
|
describe('initializeSecurityManager', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return when no servers are found', async () => {
|
||||||
|
const mockQuery = TestHelper.mockQuery<ServersModel>([]);
|
||||||
|
jest.mocked(queryAllActiveServers).mockReturnValueOnce(mockQuery);
|
||||||
|
await Actions.initializeSecurityManager();
|
||||||
|
expect(SecurityManager.init).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize SecurityManager with querying configurations', async () => {
|
||||||
|
const servers = [{url: 'server1'}, {url: 'server2'}] as ServersModel[];
|
||||||
|
const mockQuery = TestHelper.mockQuery<ServersModel>(servers);
|
||||||
|
jest.mocked(queryAllActiveServers).mockReturnValueOnce(mockQuery);
|
||||||
|
jest.mocked(DatabaseManager.getServerDatabaseAndOperator).mockImplementation((serverUrl) => ({database: `db_${serverUrl}`} as unknown as ServerDatabase));
|
||||||
|
const config = {
|
||||||
|
SiteName: 'Site',
|
||||||
|
MobileEnableBiometrics: 'true',
|
||||||
|
MobilePreventScreenCapture: 'true',
|
||||||
|
MobileJailbreakProtection: 'true',
|
||||||
|
} as unknown as ClientConfig;
|
||||||
|
jest.mocked(getSecurityConfig).mockImplementation((db) => Promise.resolve({
|
||||||
|
...config,
|
||||||
|
key: `config_${db}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Actions.initializeSecurityManager();
|
||||||
|
|
||||||
|
expect(SecurityManager.init).toHaveBeenCalledWith({
|
||||||
|
server1: {...config, key: 'config_db_server1'},
|
||||||
|
server2: {...config, key: 'config_db_server2'},
|
||||||
|
}, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should log error when querying configuration fails', async () => {
|
||||||
|
const servers = [{url: 'server1'}] as ServersModel[];
|
||||||
|
const mockQuery = TestHelper.mockQuery<ServersModel>(servers);
|
||||||
|
jest.mocked(queryAllActiveServers).mockReturnValueOnce(mockQuery);
|
||||||
|
jest.mocked(DatabaseManager.getServerDatabaseAndOperator).mockImplementation(() => {
|
||||||
|
throw new Error('test error');
|
||||||
|
});
|
||||||
|
|
||||||
|
await Actions.initializeSecurityManager();
|
||||||
|
|
||||||
|
expect(logError).toHaveBeenCalledWith('initializeSecurityManager', expect.any(Error));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests for switchToServer
|
||||||
|
describe('switchToServer', () => {
|
||||||
|
it('should log error when server is not found', async () => {
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(undefined);
|
||||||
|
await Actions.switchToServer('serverUrl', theme, intl, jest.fn());
|
||||||
|
expect(logError).toHaveBeenCalledWith('Switch to Server with url serverUrl not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch to server when lastActiveAt is set', async () => {
|
||||||
|
const server = {url: 'serverUrl', lastActiveAt: 123} as ServersModel;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(SecurityManager.isDeviceJailbroken).mockResolvedValueOnce(false);
|
||||||
|
jest.mocked(SecurityManager.authenticateWithBiometricsIfNeeded).mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await Actions.switchToServer('serverUrl', theme, intl, jest.fn());
|
||||||
|
|
||||||
|
expect(Navigation.updateProps).toHaveBeenCalledWith(Screens.HOME, {extra: undefined});
|
||||||
|
expect(DatabaseManager.setActiveServerDatabase).toHaveBeenCalledWith('serverUrl');
|
||||||
|
expect(SecurityManager.setActiveServer).toHaveBeenCalledWith('serverUrl');
|
||||||
|
expect(WebsocketManager.initializeClient).toHaveBeenCalledWith('serverUrl', 'Server Switch');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not proceed if device is jailbroken', async () => {
|
||||||
|
const server = {url: 'serverUrl', lastActiveAt: 123} as ServersModel;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(SecurityManager.isDeviceJailbroken).mockResolvedValueOnce(true);
|
||||||
|
jest.mocked(SecurityManager.authenticateWithBiometricsIfNeeded).mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await Actions.switchToServer('serverUrl', theme, intl, jest.fn());
|
||||||
|
|
||||||
|
expect(Navigation.updateProps).not.toHaveBeenCalled();
|
||||||
|
expect(DatabaseManager.setActiveServerDatabase).not.toHaveBeenCalled();
|
||||||
|
expect(SecurityManager.setActiveServer).not.toHaveBeenCalled();
|
||||||
|
expect(WebsocketManager.initializeClient).not.toHaveBeenCalled();
|
||||||
|
expect(SecurityManager.isDeviceJailbroken).toHaveBeenCalledWith('serverUrl');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tests for switchToServerAndLogin
|
||||||
|
describe('switchToServerAndLogin', () => {
|
||||||
|
it('should log error when server is not found', async () => {
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(undefined);
|
||||||
|
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||||
|
expect(logError).toHaveBeenCalledWith('Switch to Server with url serverUrl not found');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should alert server error when ping fails', async () => {
|
||||||
|
const server = {url: 'serverUrl'} as ServersModel;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(doPing).mockResolvedValueOnce({error: 'ping error'});
|
||||||
|
|
||||||
|
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||||
|
|
||||||
|
expect(alertServerError).toHaveBeenCalledWith(intl, 'ping error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should alert server error when fetching config and license fails', async () => {
|
||||||
|
const server = {url: 'serverUrl'} as ServersModel;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(doPing).mockResolvedValueOnce({});
|
||||||
|
jest.mocked(fetchConfigAndLicense).mockResolvedValueOnce({error: 'config error'});
|
||||||
|
|
||||||
|
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||||
|
|
||||||
|
expect(alertServerError).toHaveBeenCalledWith(intl, 'config error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should alert server already connected when server is already connected', async () => {
|
||||||
|
const server = {url: 'serverUrl'} as ServersModel;
|
||||||
|
const config = {DiagnosticId: 'diagId', MobileEnableBiometrics: 'true', SiteName: 'Site'} as ClientConfig;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(doPing).mockResolvedValueOnce({});
|
||||||
|
jest.mocked(fetchConfigAndLicense).mockResolvedValueOnce({config});
|
||||||
|
jest.mocked(getServerByIdentifier).mockResolvedValueOnce({lastActiveAt: 123} as ServersModel);
|
||||||
|
|
||||||
|
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||||
|
|
||||||
|
expect(alertServerAlreadyConnected).toHaveBeenCalledWith(intl);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should authenticate with biometrics and login to server', async () => {
|
||||||
|
const server = {url: 'serverUrl', displayName: 'Server'} as ServersModel;
|
||||||
|
const config = {DiagnosticId: 'diagId', MobileEnableBiometrics: 'true', SiteName: 'Site'} as ClientConfig;
|
||||||
|
const license = {} as ClientLicense;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(doPing).mockResolvedValueOnce({});
|
||||||
|
jest.mocked(fetchConfigAndLicense).mockResolvedValueOnce({config, license});
|
||||||
|
jest.mocked(getServerByIdentifier).mockResolvedValueOnce(undefined);
|
||||||
|
jest.mocked(SecurityManager.authenticateWithBiometrics).mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
await Actions.switchToServerAndLogin('serverUrl', theme, intl, jest.fn());
|
||||||
|
|
||||||
|
expect(canReceiveNotifications).toHaveBeenCalledWith('serverUrl', undefined, intl);
|
||||||
|
expect(loginToServer).toHaveBeenCalledWith(theme, 'serverUrl', 'Server', config, license);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not proceed if device is jailbroken', async () => {
|
||||||
|
const server = {url: 'serverUrl'} as ServersModel;
|
||||||
|
const config = {DiagnosticId: 'diagId', MobileJailbreakProtection: 'true'} as ClientConfig;
|
||||||
|
jest.mocked(getServer).mockResolvedValueOnce(server);
|
||||||
|
jest.mocked(doPing).mockResolvedValueOnce({});
|
||||||
|
jest.mocked(fetchConfigAndLicense).mockResolvedValueOnce({config});
|
||||||
|
jest.mocked(getServerByIdentifier).mockResolvedValueOnce(undefined);
|
||||||
|
jest.mocked(SecurityManager.isDeviceJailbroken).mockResolvedValueOnce(true);
|
||||||
|
|
||||||
|
const callback = jest.fn();
|
||||||
|
await Actions.switchToServerAndLogin('serverUrl', theme, intl, callback);
|
||||||
|
|
||||||
|
expect(SecurityManager.isDeviceJailbroken).toHaveBeenCalledWith('serverUrl');
|
||||||
|
expect(callback).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
125
app/actions/app/server.ts
Normal file
125
app/actions/app/server.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {Navigation} from 'react-native-navigation';
|
||||||
|
|
||||||
|
import {doPing} from '@actions/remote/general';
|
||||||
|
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||||
|
import {Screens} from '@constants';
|
||||||
|
import DatabaseManager from '@database/manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
|
import {getServer, getServerByIdentifier, queryAllActiveServers} from '@queries/app/servers';
|
||||||
|
import {getSecurityConfig} from '@queries/servers/system';
|
||||||
|
import {logError} from '@utils/log';
|
||||||
|
import {canReceiveNotifications} from '@utils/push_proxy';
|
||||||
|
import {alertServerAlreadyConnected, alertServerError, loginToServer} from '@utils/server';
|
||||||
|
|
||||||
|
import type {IntlShape} from 'react-intl';
|
||||||
|
|
||||||
|
export async function initializeSecurityManager() {
|
||||||
|
const servers = await queryAllActiveServers()?.fetch();
|
||||||
|
if (!servers?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises: Array<Promise<[string, SecurityClientConfig | null]>> = [];
|
||||||
|
const results: Record<string, SecurityClientConfig> = {};
|
||||||
|
|
||||||
|
for (const server of servers) {
|
||||||
|
try {
|
||||||
|
const {database} = DatabaseManager.getServerDatabaseAndOperator(server.url);
|
||||||
|
const promise = getSecurityConfig(database).then((config) => [server.url, config] as [string, SecurityClientConfig | null]);
|
||||||
|
promises.push(promise);
|
||||||
|
} catch (error) {
|
||||||
|
logError('initializeSecurityManager', error);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedConfigs = await Promise.allSettled(promises);
|
||||||
|
|
||||||
|
for (const result of resolvedConfigs) {
|
||||||
|
if (result.status === 'fulfilled') {
|
||||||
|
const [url, config] = result.value;
|
||||||
|
if (config && Object.keys(config).length > 0) { // Ensure the object is not empty
|
||||||
|
results[url] = config;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverUrl = await DatabaseManager.getActiveServerUrl();
|
||||||
|
SecurityManager.init(results, serverUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchToServer(serverUrl: string, theme: Theme, intl: IntlShape, callback?: () => void) {
|
||||||
|
const server = await getServer(serverUrl);
|
||||||
|
if (!server) {
|
||||||
|
logError(`Switch to Server with url ${serverUrl} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (server.lastActiveAt) {
|
||||||
|
const isJailbroken = await SecurityManager.isDeviceJailbroken(server.url);
|
||||||
|
if (isJailbroken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authenticated = await SecurityManager.authenticateWithBiometricsIfNeeded(server.url);
|
||||||
|
if (authenticated) {
|
||||||
|
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||||
|
DatabaseManager.setActiveServerDatabase(server.url);
|
||||||
|
SecurityManager.setActiveServer(server.url);
|
||||||
|
WebsocketManager.initializeClient(server.url, 'Server Switch');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switchToServerAndLogin(serverUrl, theme, intl, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function switchToServerAndLogin(serverUrl: string, theme: Theme, intl: IntlShape, callback?: () => void) {
|
||||||
|
const server = await getServer(serverUrl);
|
||||||
|
if (!server) {
|
||||||
|
logError(`Switch to Server with url ${serverUrl} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await doPing(server.url, true);
|
||||||
|
if (result.error) {
|
||||||
|
alertServerError(intl, result.error);
|
||||||
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await fetchConfigAndLicense(server.url, true);
|
||||||
|
if (data.error) {
|
||||||
|
alertServerError(intl, data.error);
|
||||||
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingServer = await getServerByIdentifier(data.config!.DiagnosticId);
|
||||||
|
if (existingServer && existingServer.lastActiveAt > 0) {
|
||||||
|
alertServerAlreadyConnected(intl);
|
||||||
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.config?.MobileJailbreakProtection === 'true') {
|
||||||
|
const isJailbroken = await SecurityManager.isDeviceJailbroken(server.url);
|
||||||
|
if (isJailbroken) {
|
||||||
|
callback?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let authenticated = true;
|
||||||
|
if (data.config?.MobileEnableBiometrics === 'true') {
|
||||||
|
authenticated = await SecurityManager.authenticateWithBiometrics(server.url, data.config?.SiteName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authenticated) {
|
||||||
|
canReceiveNotifications(server.url, result.canReceiveNotifications as string, intl);
|
||||||
|
loginToServer(theme, server.url, server.displayName, data.config!, data.license!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {getServerCredentials} from '@init/credentials';
|
import {getServerCredentials} from '@init/credentials';
|
||||||
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
|
|
||||||
type AfterLoginArgs = {
|
type AfterLoginArgs = {
|
||||||
|
|
@ -33,8 +34,10 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
|
||||||
|
|
||||||
const credentials = await getServerCredentials(serverUrl);
|
const credentials = await getServerCredentials(serverUrl);
|
||||||
if (credentials?.token) {
|
if (credentials?.token) {
|
||||||
|
SecurityManager.addServer(serverUrl, clData.config, true);
|
||||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||||
await WebsocketManager.initializeClient(serverUrl, 'Login');
|
await WebsocketManager.initializeClient(serverUrl, 'Login');
|
||||||
|
SecurityManager.setActiveServer(serverUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,11 @@ import {ScrollView} from 'react-native';
|
||||||
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
const edges: Edge[] = ['left', 'right', 'bottom'];
|
const edges: Edge[] = ['left', 'right', 'bottom'];
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||||
|
|
@ -36,6 +39,7 @@ const SettingContainer = ({children, testID}: SettingContainerProps) => {
|
||||||
edges={edges}
|
edges={edges}
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
testID={`${testID}.screen`}
|
testID={`${testID}.screen`}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(`${testID}.screen` as AvailableScreens)}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={styles.contentContainerStyle}
|
contentContainerStyle={styles.contentContainerStyle}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export const setServerCredentials = (serverUrl: string, token: string) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeServerCredentials = async (serverUrl: string) => {
|
export const removeServerCredentials = async (serverUrl: string) => {
|
||||||
return KeyChain.resetInternetCredentials(serverUrl);
|
return KeyChain.resetInternetCredentials({server: serverUrl});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const removeActiveServerCredentials = async () => {
|
export const removeActiveServerCredentials = async () => {
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,55 @@
|
||||||
import Emm from '@mattermost/react-native-emm';
|
import Emm from '@mattermost/react-native-emm';
|
||||||
import deepEqual from 'deep-equal';
|
import deepEqual from 'deep-equal';
|
||||||
import {isRootedExperimentalAsync} from 'expo-device';
|
import {isRootedExperimentalAsync} from 'expo-device';
|
||||||
|
import {defineMessages} from 'react-intl';
|
||||||
import {Alert, type AlertButton, AppState, type AppStateStatus, Platform} from 'react-native';
|
import {Alert, type AlertButton, AppState, type AppStateStatus, Platform} from 'react-native';
|
||||||
|
|
||||||
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||||
import {toMilliseconds} from '@utils/datetime';
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
import {isMainActivity} from '@utils/helpers';
|
import {isMainActivity} from '@utils/helpers';
|
||||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||||
|
|
||||||
const PROMPT_IN_APP_PIN_CODE_AFTER = toMilliseconds({minutes: 5});
|
const PROMPT_IN_APP_PIN_CODE_AFTER = toMilliseconds({minutes: 5});
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
blocked: {
|
||||||
|
id: 'mobile.managed.blocked_by',
|
||||||
|
defaultMessage: 'Blocked by {vendor}',
|
||||||
|
},
|
||||||
|
jailbreak: {
|
||||||
|
id: 'mobile.managed.jailbreak.emm',
|
||||||
|
defaultMessage: 'Jailbroken or rooted devices are not trusted by {vendor}.\n\nThe app will now close.',
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
id: 'mobile.managed.exit',
|
||||||
|
defaultMessage: 'Exit',
|
||||||
|
},
|
||||||
|
securedBy: {
|
||||||
|
id: 'mobile.managed.secured_by',
|
||||||
|
defaultMessage: 'Secured by {vendor}',
|
||||||
|
},
|
||||||
|
androidSettings: {
|
||||||
|
id: 'mobile.managed.settings',
|
||||||
|
defaultMessage: 'Go to settings',
|
||||||
|
},
|
||||||
|
notSecuredVendorIOS: {
|
||||||
|
id: 'mobile.managed.not_secured.ios.vendor',
|
||||||
|
defaultMessage: 'This device must be secured with biometrics or passcode to use {vendor}.\n\nGo to Settings > Face ID & Passcode.',
|
||||||
|
},
|
||||||
|
notSecuredVendorAndroid: {
|
||||||
|
id: 'mobile.managed.not_secured.android.vendor',
|
||||||
|
defaultMessage: 'This device must be secured with a screen lock to use {vendor}.',
|
||||||
|
},
|
||||||
|
notSecuredIOS: {
|
||||||
|
id: 'mobile.managed.not_secured.ios',
|
||||||
|
defaultMessage: 'This device must be secured with biometrics or passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.',
|
||||||
|
},
|
||||||
|
notSecuredAndroid: {
|
||||||
|
id: 'mobile.managed.not_secured.android',
|
||||||
|
defaultMessage: 'This device must be secured with a screen lock to use Mattermost.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
class ManagedAppSingleton {
|
class ManagedAppSingleton {
|
||||||
backgroundSince = 0;
|
backgroundSince = 0;
|
||||||
enabled = false;
|
enabled = false;
|
||||||
|
|
@ -94,11 +134,11 @@ class ManagedAppSingleton {
|
||||||
const locale = DEFAULT_LOCALE;
|
const locale = DEFAULT_LOCALE;
|
||||||
const translations = getTranslations(locale);
|
const translations = getTranslations(locale);
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
translations[messages.blocked.id].replace('{vendor}', this.vendor),
|
||||||
translations[t('mobile.managed.jailbreak')].
|
translations[messages.jailbreak.id].
|
||||||
replace('{vendor}', this.vendor),
|
replace('{vendor}', this.vendor),
|
||||||
[{
|
[{
|
||||||
text: translations[t('mobile.managed.exit')],
|
text: translations[messages.exit.id],
|
||||||
style: 'destructive',
|
style: 'destructive',
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
Emm.exitApp();
|
Emm.exitApp();
|
||||||
|
|
@ -123,7 +163,7 @@ class ManagedAppSingleton {
|
||||||
if (authExpired) {
|
if (authExpired) {
|
||||||
try {
|
try {
|
||||||
const auth = await Emm.authenticate({
|
const auth = await Emm.authenticate({
|
||||||
reason: translations[t('mobile.managed.secured_by')].replace('{vendor}', this.vendor),
|
reason: translations[messages.securedBy.id].replace('{vendor}', this.vendor),
|
||||||
fallback: true,
|
fallback: true,
|
||||||
supressEnterPassword: true,
|
supressEnterPassword: true,
|
||||||
});
|
});
|
||||||
|
|
@ -158,12 +198,12 @@ class ManagedAppSingleton {
|
||||||
};
|
};
|
||||||
|
|
||||||
showNotSecuredAlert = (translations: Record<string, string>) => {
|
showNotSecuredAlert = (translations: Record<string, string>) => {
|
||||||
return new Promise(async (resolve) => { /* eslint-disable-line no-async-promise-executor */
|
return new Promise((resolve) => {
|
||||||
const buttons: AlertButton[] = [];
|
const buttons: AlertButton[] = [];
|
||||||
|
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
text: translations[t('mobile.managed.settings')],
|
text: translations[messages.androidSettings.id],
|
||||||
onPress: () => {
|
onPress: () => {
|
||||||
Emm.openSecuritySettings();
|
Emm.openSecuritySettings();
|
||||||
},
|
},
|
||||||
|
|
@ -171,26 +211,22 @@ class ManagedAppSingleton {
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
text: translations[t('mobile.managed.exit')],
|
text: translations[messages.exit.id],
|
||||||
onPress: resolve,
|
onPress: resolve,
|
||||||
style: 'cancel',
|
style: 'cancel',
|
||||||
});
|
});
|
||||||
|
|
||||||
let message;
|
let message;
|
||||||
if (Platform.OS === 'ios') {
|
if (this.vendor) {
|
||||||
const {face} = await Emm.deviceSecureWith();
|
const platform = Platform.select({ios: messages.notSecuredVendorIOS.id, default: messages.notSecuredVendorAndroid.id});
|
||||||
|
message = translations[platform].replace('{vendor}', this.vendor);
|
||||||
if (face) {
|
|
||||||
message = translations[t('mobile.managed.not_secured.ios')];
|
|
||||||
} else {
|
|
||||||
message = translations[t('mobile.managed.not_secured.ios.touchId')];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
message = translations[t('mobile.managed.not_secured.android')];
|
const platform = Platform.select({ios: messages.notSecuredIOS.id, default: messages.notSecuredAndroid.id});
|
||||||
|
message = translations[platform];
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert.alert(
|
Alert.alert(
|
||||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
translations[messages.blocked.id].replace('{vendor}', this.vendor),
|
||||||
message,
|
message,
|
||||||
buttons,
|
buttons,
|
||||||
{cancelable: false, onDismiss: () => resolve},
|
{cancelable: false, onDismiss: () => resolve},
|
||||||
|
|
|
||||||
518
app/managers/security_manager/index.test.ts
Normal file
518
app/managers/security_manager/index.test.ts
Normal file
|
|
@ -0,0 +1,518 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import Emm from '@mattermost/react-native-emm';
|
||||||
|
import {isRootedExperimentalAsync} from 'expo-device';
|
||||||
|
import {Alert, type AppStateStatus} from 'react-native';
|
||||||
|
|
||||||
|
import {switchToServer} from '@actions/app/server';
|
||||||
|
import {logout} from '@actions/remote/session';
|
||||||
|
import {Screens} from '@constants';
|
||||||
|
import {DEFAULT_LOCALE, getTranslations, t} from '@i18n';
|
||||||
|
import {getServerCredentials} from '@init/credentials';
|
||||||
|
import TestHelper from '@test/test_helper';
|
||||||
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
|
import {logError} from '@utils/log';
|
||||||
|
|
||||||
|
import SecurityManager from '.';
|
||||||
|
|
||||||
|
jest.mock('@mattermost/react-native-emm', () => ({
|
||||||
|
isDeviceSecured: jest.fn(),
|
||||||
|
authenticate: jest.fn(),
|
||||||
|
openSecuritySettings: jest.fn(),
|
||||||
|
exitApp: jest.fn(),
|
||||||
|
enableBlurScreen: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('expo-device', () => ({
|
||||||
|
isRootedExperimentalAsync: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('@actions/app/server', () => ({switchToServer: jest.fn()}));
|
||||||
|
jest.mock('@actions/remote/session', () => ({logout: jest.fn()}));
|
||||||
|
jest.mock('@utils/datetime', () => ({toMilliseconds: jest.fn(() => 25000)}));
|
||||||
|
jest.mock('@utils/helpers', () => ({
|
||||||
|
isMainActivity: jest.fn(() => true),
|
||||||
|
isTablet: jest.fn(() => false),
|
||||||
|
}));
|
||||||
|
jest.mock('@utils/log', () => ({logError: jest.fn()}));
|
||||||
|
jest.mock('@init/managed_app', () => ({enabled: true, inAppPinCode: false}));
|
||||||
|
jest.mock('@init/credentials', () => ({getServerCredentials: jest.fn().mockResolvedValue({token: 'token'})}));
|
||||||
|
|
||||||
|
describe('SecurityManager', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
SecurityManager.initialized = false;
|
||||||
|
SecurityManager.serverConfig = {};
|
||||||
|
SecurityManager.activeServer = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('init', () => {
|
||||||
|
test('should initialize with servers', () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true'},
|
||||||
|
};
|
||||||
|
SecurityManager.init(servers, 'server-1');
|
||||||
|
expect(SecurityManager.serverConfig['server-1']).toBeDefined();
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should initialize with servers and set active server', async () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true'},
|
||||||
|
'server-2': {SiteName: 'Server Two', MobileEnableBiometrics: 'false'},
|
||||||
|
};
|
||||||
|
await SecurityManager.init(servers, 'server-1');
|
||||||
|
expect(SecurityManager.serverConfig['server-1']).toBeDefined();
|
||||||
|
expect(SecurityManager.serverConfig['server-2']).toBeDefined();
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not reinitialize if already initialized', async () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true'},
|
||||||
|
};
|
||||||
|
SecurityManager.initialized = true;
|
||||||
|
await SecurityManager.init(servers, 'server-1');
|
||||||
|
expect(SecurityManager.serverConfig['server-1']).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should set active server and authenticate if not jailbroken', async () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true'},
|
||||||
|
};
|
||||||
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
||||||
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const isDeviceJailbrokenSpy = jest.spyOn(SecurityManager, 'isDeviceJailbroken');
|
||||||
|
const authenticateWithBiometricsIfNeededSpy = jest.spyOn(SecurityManager, 'authenticateWithBiometricsIfNeeded');
|
||||||
|
|
||||||
|
await SecurityManager.init(servers, 'server-1');
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-1');
|
||||||
|
expect(isDeviceJailbrokenSpy).toHaveBeenCalledWith('server-1');
|
||||||
|
expect(authenticateWithBiometricsIfNeededSpy).toHaveBeenCalledWith('server-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not authenticate if device is jailbroken', async () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true', MobileJailbreakProtection: 'true'},
|
||||||
|
};
|
||||||
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(true);
|
||||||
|
await SecurityManager.init(servers, 'server-1');
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-1');
|
||||||
|
expect(SecurityManager.isDeviceJailbroken).toHaveBeenCalledWith('server-1');
|
||||||
|
expect(SecurityManager.authenticateWithBiometricsIfNeeded).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not set active server if not provided', async () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true'},
|
||||||
|
};
|
||||||
|
await SecurityManager.init(servers);
|
||||||
|
expect(SecurityManager.activeServer).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addServer', () => {
|
||||||
|
test('should add server config with biometrics enabled', () => {
|
||||||
|
SecurityManager.addServer('server-1', {SiteName: 'Server One', MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
expect(SecurityManager.serverConfig['server-1']).toEqual({
|
||||||
|
siteName: 'Server One',
|
||||||
|
Biometrics: true,
|
||||||
|
JailbreakProtection: false,
|
||||||
|
PreventScreenCapture: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add server config with jailbreak protection enabled', () => {
|
||||||
|
SecurityManager.addServer('server-2', {SiteName: 'Server Two', MobileJailbreakProtection: 'true'} as ClientConfig);
|
||||||
|
expect(SecurityManager.serverConfig['server-2']).toEqual({
|
||||||
|
siteName: 'Server Two',
|
||||||
|
Biometrics: false,
|
||||||
|
JailbreakProtection: true,
|
||||||
|
PreventScreenCapture: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add server config with screen capture prevention enabled', () => {
|
||||||
|
SecurityManager.addServer('server-3', {SiteName: 'Server Three', MobilePreventScreenCapture: 'true'} as ClientConfig);
|
||||||
|
expect(SecurityManager.serverConfig['server-3']).toEqual({
|
||||||
|
siteName: 'Server Three',
|
||||||
|
Biometrics: false,
|
||||||
|
JailbreakProtection: false,
|
||||||
|
PreventScreenCapture: true,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add server config with all features enabled', () => {
|
||||||
|
SecurityManager.addServer('server-4', {
|
||||||
|
SiteName: 'Server Four',
|
||||||
|
MobileEnableBiometrics: 'true',
|
||||||
|
MobileJailbreakProtection: 'true',
|
||||||
|
MobilePreventScreenCapture: 'true',
|
||||||
|
} as ClientConfig);
|
||||||
|
expect(SecurityManager.serverConfig['server-4']).toEqual({
|
||||||
|
siteName: 'Server Four',
|
||||||
|
Biometrics: true,
|
||||||
|
JailbreakProtection: true,
|
||||||
|
PreventScreenCapture: true,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add server config with authenticated set to true', () => {
|
||||||
|
SecurityManager.addServer('server-5', {SiteName: 'Server Five'} as ClientConfig, true);
|
||||||
|
expect(SecurityManager.serverConfig['server-5']).toEqual({
|
||||||
|
siteName: 'Server Five',
|
||||||
|
Biometrics: false,
|
||||||
|
JailbreakProtection: false,
|
||||||
|
PreventScreenCapture: false,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should add server config without config', () => {
|
||||||
|
SecurityManager.addServer('server-6');
|
||||||
|
expect(SecurityManager.serverConfig['server-6']).toEqual({
|
||||||
|
siteName: undefined,
|
||||||
|
Biometrics: false,
|
||||||
|
JailbreakProtection: false,
|
||||||
|
PreventScreenCapture: false,
|
||||||
|
authenticated: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update a server config previously added', () => {
|
||||||
|
SecurityManager.addServer('server-3', {SiteName: 'Server Three', MobilePreventScreenCapture: 'true'} as ClientConfig, true);
|
||||||
|
expect(SecurityManager.serverConfig['server-3']).toEqual({
|
||||||
|
siteName: 'Server Three',
|
||||||
|
Biometrics: false,
|
||||||
|
JailbreakProtection: false,
|
||||||
|
PreventScreenCapture: true,
|
||||||
|
authenticated: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeServer', () => {
|
||||||
|
test('should remove server config and active server', async () => {
|
||||||
|
const servers: Record<string, any> = {
|
||||||
|
'server-1': {SiteName: 'Server One', MobileEnableBiometrics: 'true'},
|
||||||
|
'server-2': {SiteName: 'Server Two', MobileEnableBiometrics: 'false'},
|
||||||
|
};
|
||||||
|
await SecurityManager.init(servers, 'server-1');
|
||||||
|
SecurityManager.removeServer('server-1');
|
||||||
|
expect(SecurityManager.serverConfig['server-1']).toBeUndefined();
|
||||||
|
expect(SecurityManager.activeServer).toBeUndefined();
|
||||||
|
expect(SecurityManager.initialized).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should remove server config', () => {
|
||||||
|
SecurityManager.addServer('server-3');
|
||||||
|
SecurityManager.removeServer('server-3');
|
||||||
|
expect(SecurityManager.serverConfig['server-3']).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getServerConfig', () => {
|
||||||
|
test('should return server config', () => {
|
||||||
|
SecurityManager.addServer('server-4', {SiteName: 'Server Four'} as ClientConfig);
|
||||||
|
expect(SecurityManager.getServerConfig('server-4')?.siteName).toBe('Server Four');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return undefined if server config does not exist', () => {
|
||||||
|
expect(SecurityManager.getServerConfig('server-5')).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setActiveServer', () => {
|
||||||
|
test('should set active server and update lastAccessed', () => {
|
||||||
|
SecurityManager.addServer('server-5');
|
||||||
|
const before = Date.now();
|
||||||
|
SecurityManager.setActiveServer('server-5');
|
||||||
|
const after = Date.now();
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-5');
|
||||||
|
expect(SecurityManager.serverConfig['server-5'].lastAccessed).toBeGreaterThanOrEqual(before);
|
||||||
|
expect(SecurityManager.serverConfig['server-5'].lastAccessed).toBeLessThanOrEqual(after);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not set active server if server does not exist', () => {
|
||||||
|
SecurityManager.setActiveServer('server-6');
|
||||||
|
expect(SecurityManager.activeServer).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should update active server and lastAccessed if a different server is set', () => {
|
||||||
|
SecurityManager.addServer('server-7');
|
||||||
|
SecurityManager.addServer('server-8');
|
||||||
|
SecurityManager.setActiveServer('server-7');
|
||||||
|
const before = Date.now();
|
||||||
|
SecurityManager.setActiveServer('server-8');
|
||||||
|
const after = Date.now();
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-8');
|
||||||
|
expect(SecurityManager.serverConfig['server-8'].lastAccessed).toBeGreaterThanOrEqual(before);
|
||||||
|
expect(SecurityManager.serverConfig['server-8'].lastAccessed).toBeLessThanOrEqual(after);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not change active server or lastAccessed if the same server is set', async () => {
|
||||||
|
SecurityManager.addServer('server-9');
|
||||||
|
SecurityManager.setActiveServer('server-9');
|
||||||
|
const lastAccessed = SecurityManager.serverConfig['server-9'].lastAccessed;
|
||||||
|
SecurityManager.setActiveServer('server-9');
|
||||||
|
expect(SecurityManager.activeServer).toBe('server-9');
|
||||||
|
expect(SecurityManager.serverConfig['server-9'].lastAccessed).toBe(lastAccessed);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isScreenCapturePrevented', () => {
|
||||||
|
test('should return true if screen capture prevention is enabled', () => {
|
||||||
|
SecurityManager.addServer('server-1', {SiteName: 'Server One', MobilePreventScreenCapture: 'true'} as ClientConfig);
|
||||||
|
expect(SecurityManager.isScreenCapturePrevented('server-1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false if screen capture prevention is disabled', () => {
|
||||||
|
SecurityManager.addServer('server-2', {SiteName: 'Server Two', MobilePreventScreenCapture: 'false'} as ClientConfig);
|
||||||
|
expect(SecurityManager.isScreenCapturePrevented('server-2')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false if server configuration does not exist', () => {
|
||||||
|
expect(SecurityManager.isScreenCapturePrevented('server-3')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false if PreventScreenCapture property is not set', () => {
|
||||||
|
SecurityManager.addServer('server-4', {SiteName: 'Server Four'} as ClientConfig);
|
||||||
|
expect(SecurityManager.isScreenCapturePrevented('server-4')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('authenticateWithBiometricsIfNeeded', () => {
|
||||||
|
test('should handle biometric authentication if biometrics enabled and device secured', async () => {
|
||||||
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
||||||
|
jest.mocked(Emm.authenticate).mockResolvedValue(true);
|
||||||
|
SecurityManager.addServer('server-6', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-6')).resolves.toBe(true);
|
||||||
|
expect(Emm.isDeviceSecured).toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not prompt for biometric authentication if biometrics enabled but device is not secured', async () => {
|
||||||
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(false);
|
||||||
|
const showNotSecuredAlertSpy = jest.spyOn(SecurityManager, 'showNotSecuredAlert');
|
||||||
|
SecurityManager.addServer('server-6', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-6')).resolves.toBe(false);
|
||||||
|
expect(showNotSecuredAlertSpy).toHaveBeenCalled();
|
||||||
|
expect(Emm.isDeviceSecured).toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not attempt biometric authentication if biometrics not enabled', async () => {
|
||||||
|
SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'false'} as ClientConfig);
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-8')).resolves.toBe(true);
|
||||||
|
expect(Emm.isDeviceSecured).not.toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve with true if biometric authentication succeeds', async () => {
|
||||||
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
||||||
|
jest.mocked(Emm.authenticate).mockResolvedValue(true);
|
||||||
|
SecurityManager.addServer('server-9', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-9')).resolves.toBe(true);
|
||||||
|
expect(Emm.isDeviceSecured).toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should log error and resolve with false if biometric authentication fails', async () => {
|
||||||
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
||||||
|
jest.mocked(Emm.authenticate).mockResolvedValue(false);
|
||||||
|
SecurityManager.addServer('server-10', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-10')).resolves.toBe(false);
|
||||||
|
expect(Emm.isDeviceSecured).toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).toHaveBeenCalled();
|
||||||
|
expect(SecurityManager.getServerConfig('server-10')?.authenticated).toBe(false);
|
||||||
|
expect(logError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should log error and resolve with false if biometric authentication throws an error', async () => {
|
||||||
|
jest.mocked(Emm.isDeviceSecured).mockResolvedValue(true);
|
||||||
|
jest.mocked(Emm.authenticate).mockRejectedValue(new Error('Authorization cancelled'));
|
||||||
|
SecurityManager.addServer('server-11', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-11')).resolves.toBe(false);
|
||||||
|
expect(Emm.isDeviceSecured).toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).toHaveBeenCalled();
|
||||||
|
expect(logError).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not attempt biometric authentication if server was previously authenticated within 5 mins', async () => {
|
||||||
|
SecurityManager.addServer('server-12', {MobileEnableBiometrics: 'true'} as ClientConfig, true);
|
||||||
|
SecurityManager.serverConfig['server-12'].lastAccessed = Date.now() - toMilliseconds({minutes: 1});
|
||||||
|
await expect(SecurityManager.authenticateWithBiometricsIfNeeded('server-12')).resolves.toBe(true);
|
||||||
|
expect(Emm.isDeviceSecured).not.toHaveBeenCalled();
|
||||||
|
expect(Emm.authenticate).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not attempt biometric authentication if server was previously failed authentication even though lastAccess is less than 5 mins', async () => {
|
||||||
|
SecurityManager.addServer('server-13', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.serverConfig['server-13'].authenticated = false;
|
||||||
|
SecurityManager.serverConfig['server-13'].lastAccessed = Date.now() - toMilliseconds({minutes: 1});
|
||||||
|
await SecurityManager.authenticateWithBiometricsIfNeeded('server-13');
|
||||||
|
expect(SecurityManager.serverConfig['server-13'].authenticated).toBe(false);
|
||||||
|
await expect(Emm.authenticate).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onAppStateChange', () => {
|
||||||
|
test('should handle app state changes', async () => {
|
||||||
|
SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.setActiveServer('server-8');
|
||||||
|
await SecurityManager.onAppStateChange('background' as AppStateStatus);
|
||||||
|
expect(SecurityManager.backgroundSince).toBeGreaterThan(0);
|
||||||
|
await SecurityManager.onAppStateChange('active' as AppStateStatus);
|
||||||
|
expect(SecurityManager.backgroundSince).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should call biometric authentication app state changes', async () => {
|
||||||
|
const authenticateWithBiometrics = jest.spyOn(SecurityManager, 'authenticateWithBiometrics');
|
||||||
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(false);
|
||||||
|
SecurityManager.addServer('server-8', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.setActiveServer('server-8');
|
||||||
|
SecurityManager.onAppStateChange('background' as AppStateStatus);
|
||||||
|
SecurityManager.backgroundSince = Date.now() - toMilliseconds({minutes: 5, seconds: 1});
|
||||||
|
SecurityManager.onAppStateChange('active' as AppStateStatus);
|
||||||
|
await TestHelper.wait(300);
|
||||||
|
expect(authenticateWithBiometrics).toHaveBeenCalledWith('server-8');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showNotSecuredAlert', () => {
|
||||||
|
test('should show not secured alert', async () => {
|
||||||
|
SecurityManager.addServer('server-9');
|
||||||
|
SecurityManager.showNotSecuredAlert('server-9', 'Test Site', getTranslations(DEFAULT_LOCALE));
|
||||||
|
await TestHelper.wait(300);
|
||||||
|
expect(Alert.alert).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('goToPreviousServer', () => {
|
||||||
|
test('should switch to previous server', async () => {
|
||||||
|
SecurityManager.activeServer = undefined;
|
||||||
|
SecurityManager.addServer('server-10', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.setActiveServer('server-10');
|
||||||
|
SecurityManager.addServer('server-11', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.setActiveServer('server-11');
|
||||||
|
await SecurityManager.goToPreviousServer(['server-10', 'server-11']);
|
||||||
|
expect(switchToServer).toHaveBeenCalledWith('server-10', expect.anything(), expect.anything());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildAlertOptions', () => {
|
||||||
|
test('should build alert options with logout', async () => {
|
||||||
|
jest.mocked(getServerCredentials).mockResolvedValue({token: 'some-token', serverUrl: 'server-12', userId: 'me'});
|
||||||
|
SecurityManager.addServer('server-12', {MobileEnableBiometrics: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.setActiveServer('server-12');
|
||||||
|
const translations = getTranslations(DEFAULT_LOCALE);
|
||||||
|
const buttons = await SecurityManager.buildAlertOptions('server-12', translations);
|
||||||
|
expect(buttons.length).toBeGreaterThan(0);
|
||||||
|
const logoutButton = buttons.find((button) => button.text === translations[t('mobile.managed.logout')]);
|
||||||
|
expect(logoutButton).toBeDefined();
|
||||||
|
logoutButton?.onPress?.();
|
||||||
|
expect(logout).toHaveBeenCalledWith('server-12', undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showDeviceNotTrustedAlert', () => {
|
||||||
|
test('should show device not trusted alert', async () => {
|
||||||
|
const server = 'server-13';
|
||||||
|
const siteName = 'Site Name';
|
||||||
|
const translations = getTranslations(DEFAULT_LOCALE);
|
||||||
|
|
||||||
|
await SecurityManager.showDeviceNotTrustedAlert(server, siteName, translations);
|
||||||
|
|
||||||
|
expect(Alert.alert).toHaveBeenCalledWith(
|
||||||
|
translations[t('mobile.managed.blocked_by')].replace('{vendor}', siteName),
|
||||||
|
translations[t('mobile.managed.jailbreak')].replace('{vendor}', siteName),
|
||||||
|
expect.any(Array),
|
||||||
|
{cancelable: false},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('showBiometricFailureAlert', () => {
|
||||||
|
test('should show biometric failure alert', async () => {
|
||||||
|
const server = 'server-14';
|
||||||
|
const siteName = 'Site Name';
|
||||||
|
const translations = getTranslations(DEFAULT_LOCALE);
|
||||||
|
|
||||||
|
await SecurityManager.showBiometricFailureAlert(server, false, siteName, translations);
|
||||||
|
|
||||||
|
expect(Alert.alert).toHaveBeenCalledWith(
|
||||||
|
translations[t('mobile.managed.blocked_by')].replace('{vendor}', siteName),
|
||||||
|
translations[t('mobile.managed.biometric_failed')],
|
||||||
|
expect.any(Array),
|
||||||
|
{cancelable: false},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isDeviceJailbroken', () => {
|
||||||
|
test('should check if device is jailbroken', async () => {
|
||||||
|
const server = 'server-15';
|
||||||
|
const siteName = 'Site Name';
|
||||||
|
SecurityManager.addServer(server, {MobileJailbreakProtection: 'true'} as ClientConfig);
|
||||||
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(true);
|
||||||
|
const translations = getTranslations(DEFAULT_LOCALE);
|
||||||
|
|
||||||
|
const result = await SecurityManager.isDeviceJailbroken(server, siteName);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
await TestHelper.wait(300);
|
||||||
|
expect(Alert.alert).toHaveBeenCalledWith(
|
||||||
|
translations[t('mobile.managed.blocked_by')].replace('{vendor}', siteName),
|
||||||
|
translations[t('mobile.managed.jailbreak')].replace('{vendor}', siteName),
|
||||||
|
expect.any(Array),
|
||||||
|
{cancelable: false},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return false if device is not jailbroken', async () => {
|
||||||
|
const server = 'server-16';
|
||||||
|
const siteName = 'Site Name';
|
||||||
|
SecurityManager.addServer(server, {MobileJailbreakProtection: 'true'} as ClientConfig);
|
||||||
|
jest.mocked(isRootedExperimentalAsync).mockResolvedValue(false);
|
||||||
|
|
||||||
|
const result = await SecurityManager.isDeviceJailbroken(server, siteName);
|
||||||
|
|
||||||
|
expect(result).toBe(false);
|
||||||
|
expect(Alert.alert).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getShieldScreenId', () => {
|
||||||
|
test('should return the name of the screen shielded if prevent screen capture is enabled', () => {
|
||||||
|
SecurityManager.addServer('server-2', {MobilePreventScreenCapture: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.activeServer = 'server-2';
|
||||||
|
expect(SecurityManager.getShieldScreenId(Screens.CHANNEL)).toBe(`${Screens.CHANNEL}.screen.shielded`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the name of the screen without shielded if active server is different', () => {
|
||||||
|
SecurityManager.addServer('server-2', {MobilePreventScreenCapture: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.activeServer = 'server-1';
|
||||||
|
expect(SecurityManager.getShieldScreenId(Screens.CHANNEL)).toBe(`${Screens.CHANNEL}.screen`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the name of the screen shielded if prevent screen capture is disabled', () => {
|
||||||
|
SecurityManager.addServer('server-2', {MobilePreventScreenCapture: 'false'} as ClientConfig);
|
||||||
|
expect(SecurityManager.getShieldScreenId(Screens.CHANNEL)).toBe(`${Screens.CHANNEL}.screen`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the name of the screen shielded if prevent screen capture is disabled but forced', () => {
|
||||||
|
SecurityManager.addServer('server-2', {MobilePreventScreenCapture: 'false'} as ClientConfig);
|
||||||
|
expect(SecurityManager.getShieldScreenId(Screens.CHANNEL, true)).toBe(`${Screens.CHANNEL}.screen.shielded`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should return the name of the screen as shielded but skip', () => {
|
||||||
|
SecurityManager.addServer('server-2', {MobilePreventScreenCapture: 'true'} as ClientConfig);
|
||||||
|
SecurityManager.activeServer = 'server-2';
|
||||||
|
expect(SecurityManager.getShieldScreenId(Screens.CHANNEL, false, true)).toBe(`${Screens.CHANNEL}.screen.skip.shielded`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
515
app/managers/security_manager/index.ts
Normal file
515
app/managers/security_manager/index.ts
Normal file
|
|
@ -0,0 +1,515 @@
|
||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import Emm from '@mattermost/react-native-emm';
|
||||||
|
import {isRootedExperimentalAsync} from 'expo-device';
|
||||||
|
import {createIntl, defineMessages} from 'react-intl';
|
||||||
|
import {Alert, type AlertButton, AppState, type AppStateStatus, Platform} from 'react-native';
|
||||||
|
|
||||||
|
import {switchToServer} from '@actions/app/server';
|
||||||
|
import {logout} from '@actions/remote/session';
|
||||||
|
import {Preferences} from '@constants';
|
||||||
|
import {DEFAULT_LOCALE, getTranslations} from '@i18n';
|
||||||
|
import {getServerCredentials} from '@init/credentials';
|
||||||
|
import ManagedApp from '@init/managed_app';
|
||||||
|
import {toMilliseconds} from '@utils/datetime';
|
||||||
|
import {isMainActivity} from '@utils/helpers';
|
||||||
|
import {logError} from '@utils/log';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
|
type SecurityManagerServerConfig = {
|
||||||
|
Biometrics?: boolean;
|
||||||
|
JailbreakProtection?: boolean;
|
||||||
|
PreventScreenCapture?: boolean;
|
||||||
|
authenticated?: boolean;
|
||||||
|
lastAccessed?: number;
|
||||||
|
siteName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SecurityManagerServersCollection = Record<string, SecurityManagerServerConfig>;
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
not_secured_vendor_ios: {
|
||||||
|
id: 'mobile.managed.not_secured.ios.vendor',
|
||||||
|
defaultMessage: 'This device must be secured with biometrics or passcode to use {vendor}.\n\nGo to Settings > Face ID & Passcode.',
|
||||||
|
},
|
||||||
|
not_secured_vendor_android: {
|
||||||
|
id: 'mobile.managed.not_secured.android.vendor',
|
||||||
|
defaultMessage: 'This device must be secured with a screen lock to use {vendor}.',
|
||||||
|
},
|
||||||
|
not_secured_ios: {
|
||||||
|
id: 'mobile.managed.not_secured.ios',
|
||||||
|
defaultMessage: 'This device must be secured with biometrics or passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.',
|
||||||
|
},
|
||||||
|
not_secured_android: {
|
||||||
|
id: 'mobile.managed.not_secured.android',
|
||||||
|
defaultMessage: 'This device must be secured with a screen lock to use Mattermost.',
|
||||||
|
},
|
||||||
|
blocked_by: {
|
||||||
|
id: 'mobile.managed.blocked_by',
|
||||||
|
defaultMessage: 'Blocked by {vendor}',
|
||||||
|
},
|
||||||
|
androidSettings: {
|
||||||
|
id: 'mobile.managed.settings',
|
||||||
|
defaultMessage: 'Go to settings',
|
||||||
|
},
|
||||||
|
securedBy: {
|
||||||
|
id: 'mobile.managed.secured_by',
|
||||||
|
defaultMessage: 'Secured by {vendor}',
|
||||||
|
},
|
||||||
|
logout: {
|
||||||
|
id: 'mobile.managed.logout',
|
||||||
|
defaultMessage: 'Logout',
|
||||||
|
},
|
||||||
|
ok: {
|
||||||
|
id: 'mobile.managed.OK',
|
||||||
|
defaultMessage: 'OK',
|
||||||
|
},
|
||||||
|
switchServer: {
|
||||||
|
id: 'mobile.managed.switch_server',
|
||||||
|
defaultMessage: 'Switch server',
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
id: 'mobile.managed.exit',
|
||||||
|
defaultMessage: 'Exit',
|
||||||
|
},
|
||||||
|
jailbreak: {
|
||||||
|
id: 'mobile.managed.jailbreak',
|
||||||
|
defaultMessage: 'Jailbroken or rooted devices are not trusted by {vendor}.',
|
||||||
|
},
|
||||||
|
biometric_failed: {
|
||||||
|
id: 'mobile.managed.biometric_failed',
|
||||||
|
defaultMessage: 'Biometric or Passcode authentication failed.',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
class SecurityManagerSingleton {
|
||||||
|
activeServer?: string;
|
||||||
|
serverConfig: SecurityManagerServersCollection = {};
|
||||||
|
backgroundSince = 0;
|
||||||
|
previousAppState?: AppStateStatus;
|
||||||
|
initialized = false;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
AppState.addEventListener('change', this.onAppStateChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the class with existing servers on app launch.
|
||||||
|
* Should be called when the app starts.
|
||||||
|
*/
|
||||||
|
async init(servers: Record<string, SecurityClientConfig>, activeServer?: string) {
|
||||||
|
if (this.initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.initialized = true;
|
||||||
|
const added = new Set<string>();
|
||||||
|
for (const [server, config] of Object.entries(servers)) {
|
||||||
|
if (!this.serverConfig[server]) {
|
||||||
|
this.addServer(server, config);
|
||||||
|
added.add(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeServer && (!added.has(activeServer) || !this.activeServer)) {
|
||||||
|
this.activeServer = activeServer;
|
||||||
|
this.setScreenCapturePolicy(activeServer);
|
||||||
|
const isJailbroken = await this.isDeviceJailbroken(activeServer);
|
||||||
|
if (!isJailbroken) {
|
||||||
|
this.authenticateWithBiometricsIfNeeded(activeServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles app state changes to prompt authentication when resuming from background.
|
||||||
|
*/
|
||||||
|
onAppStateChange = async (appState: AppStateStatus) => {
|
||||||
|
if (this.isAuthenticationHandledByEmm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isActive = appState === 'active';
|
||||||
|
const isBackground = appState === 'background';
|
||||||
|
|
||||||
|
if (isActive && this.previousAppState === 'background') {
|
||||||
|
if (this.activeServer) {
|
||||||
|
const config = this.getServerConfig(this.activeServer);
|
||||||
|
if (config && config.Biometrics && isMainActivity()) {
|
||||||
|
const authExpired = this.backgroundSince > 0 && (Date.now() - this.backgroundSince) >= toMilliseconds({minutes: 5});
|
||||||
|
if (authExpired) {
|
||||||
|
const isJailbroken = await this.isDeviceJailbroken(this.activeServer);
|
||||||
|
if (!isJailbroken) {
|
||||||
|
await this.authenticateWithBiometrics(this.activeServer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.backgroundSince = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (isBackground) {
|
||||||
|
this.backgroundSince = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.previousAppState = appState;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if EMM is already enabled and setup
|
||||||
|
* to handle biometric / passcode authentication.
|
||||||
|
*/
|
||||||
|
isAuthenticationHandledByEmm = () => {
|
||||||
|
return ManagedApp.enabled && ManagedApp.inAppPinCode;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if EMM is already enabled and setup
|
||||||
|
* to handle jailbreak protection.
|
||||||
|
*/
|
||||||
|
isJalbreakProtectionHandledByEmm = () => {
|
||||||
|
return ManagedApp.enabled && ManagedApp.cacheConfig?.jailbreakProtection === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if EMM is already enabled and setup
|
||||||
|
* to handle screenshot protection.
|
||||||
|
*/
|
||||||
|
isScreenshotProtectionHandledByEmm = () => {
|
||||||
|
return ManagedApp.enabled && ManagedApp.cacheConfig?.blurApplicationScreen === 'true';
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configuration of a server to prevent screenshots.
|
||||||
|
*/
|
||||||
|
isScreenCapturePrevented = (server: string) => {
|
||||||
|
const config = this.getServerConfig(server);
|
||||||
|
if (!config) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return config.PreventScreenCapture == null ? false : config.PreventScreenCapture;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the device is Jailbroken or Rooted.
|
||||||
|
*/
|
||||||
|
isDeviceJailbroken = async (server: string, siteName?: string) => {
|
||||||
|
if (this.isJalbreakProtectionHandledByEmm()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.getServerConfig(server);
|
||||||
|
if (!config && !siteName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = DEFAULT_LOCALE;
|
||||||
|
const translations = getTranslations(locale);
|
||||||
|
if (config?.JailbreakProtection || siteName) {
|
||||||
|
const isRooted = await isRootedExperimentalAsync();
|
||||||
|
if (isRooted) {
|
||||||
|
this.showDeviceNotTrustedAlert(server, siteName, translations);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the config for a server.
|
||||||
|
*/
|
||||||
|
addServer = (server: string, config?: SecurityClientConfig, authenticated = false) => {
|
||||||
|
const mobileConfig: SecurityManagerServerConfig = {
|
||||||
|
siteName: config?.SiteName,
|
||||||
|
Biometrics: config?.MobileEnableBiometrics === 'true',
|
||||||
|
JailbreakProtection: config?.MobileJailbreakProtection === 'true',
|
||||||
|
PreventScreenCapture: config?.MobilePreventScreenCapture === 'true',
|
||||||
|
authenticated,
|
||||||
|
};
|
||||||
|
this.serverConfig[server] = mobileConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a configured server, to be called on logout.
|
||||||
|
*/
|
||||||
|
removeServer = async (server: string) => {
|
||||||
|
delete this.serverConfig[server];
|
||||||
|
if (server === this.activeServer) {
|
||||||
|
this.initialized = false;
|
||||||
|
this.activeServer = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configuration of a server.
|
||||||
|
*/
|
||||||
|
getServerConfig = (server: string): SecurityManagerServerConfig| undefined => {
|
||||||
|
return this.serverConfig[server];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches the active server.
|
||||||
|
*/
|
||||||
|
setActiveServer = (server: string) => {
|
||||||
|
if (this.activeServer === server) {
|
||||||
|
// active server is not changing, so no need to do anything here
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activeServer && this.serverConfig[this.activeServer]) {
|
||||||
|
this.serverConfig[this.activeServer].lastAccessed = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.serverConfig[server]) {
|
||||||
|
this.activeServer = server;
|
||||||
|
this.serverConfig[server].lastAccessed = Date.now();
|
||||||
|
this.setScreenCapturePolicy(server);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last accessed server.
|
||||||
|
*/
|
||||||
|
getLastAccessedServer = (otherServers: string[]) => {
|
||||||
|
const lastAccessed = otherServers.map((s) => this.serverConfig[s].lastAccessed).sort().reverse()[0];
|
||||||
|
return Object.keys(this.serverConfig).find((s) => this.serverConfig[s].lastAccessed === lastAccessed);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switches to the previous server.
|
||||||
|
*/
|
||||||
|
goToPreviousServer = async (otherServers: string[]) => {
|
||||||
|
// Switch to last accessed server
|
||||||
|
const lastAccessedServer = this.getLastAccessedServer(otherServers);
|
||||||
|
if (lastAccessedServer) {
|
||||||
|
const theme = Preferences.THEMES.denim;
|
||||||
|
const locale = DEFAULT_LOCALE;
|
||||||
|
|
||||||
|
const intl = createIntl({
|
||||||
|
locale,
|
||||||
|
defaultLocale: DEFAULT_LOCALE,
|
||||||
|
messages: getTranslations(locale),
|
||||||
|
});
|
||||||
|
switchToServer(lastAccessedServer, theme, intl);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if biometric authentication should be prompted.
|
||||||
|
*/
|
||||||
|
authenticateWithBiometricsIfNeeded = async (server: string) => {
|
||||||
|
if (this.isAuthenticationHandledByEmm()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.getServerConfig(server);
|
||||||
|
if (!config) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config?.Biometrics) {
|
||||||
|
const lastAccessed = config?.lastAccessed ?? 0;
|
||||||
|
const timeSinceLastAccessed = Date.now() - lastAccessed;
|
||||||
|
if (timeSinceLastAccessed > toMilliseconds({minutes: 5}) || config.authenticated === false) {
|
||||||
|
return this.authenticateWithBiometrics(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles biometric authentication.
|
||||||
|
*/
|
||||||
|
authenticateWithBiometrics = async (server: string, siteName?: string) => {
|
||||||
|
if (this.isAuthenticationHandledByEmm()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this.getServerConfig(server);
|
||||||
|
if (!config && !siteName) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const locale = DEFAULT_LOCALE;
|
||||||
|
const translations = getTranslations(locale);
|
||||||
|
|
||||||
|
const isSecured = await Emm.isDeviceSecured();
|
||||||
|
if (!isSecured) {
|
||||||
|
await this.showNotSecuredAlert(server, siteName, translations);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const shouldBlurOnAuthenticate = server === this.activeServer && this.isScreenCapturePrevented(server);
|
||||||
|
try {
|
||||||
|
const auth = await Emm.authenticate({
|
||||||
|
reason: translations[messages.securedBy.id].replace('{vendor}', siteName || config?.siteName || 'Mattermost'),
|
||||||
|
fallback: true,
|
||||||
|
supressEnterPassword: true,
|
||||||
|
blurOnAuthenticate: shouldBlurOnAuthenticate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
config.authenticated = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth) {
|
||||||
|
throw new Error('Authorization cancelled');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logError('Failed to authenticate with biometrics', err);
|
||||||
|
this.showBiometricFailureAlert(server, shouldBlurOnAuthenticate, siteName, translations);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the screen capture policy for the given server.
|
||||||
|
*/
|
||||||
|
setScreenCapturePolicy = (server: string) => {
|
||||||
|
if (this.isScreenshotProtectionHandledByEmm()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Emm.enableBlurScreen(this.isScreenCapturePrevented(server));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the shielded screen ID for the screen.
|
||||||
|
*/
|
||||||
|
getShieldScreenId = (screen: AvailableScreens, force = false, skip = false) => {
|
||||||
|
if ((this.activeServer && this.isScreenCapturePrevented(this.activeServer)) || force) {
|
||||||
|
const name = `${screen}.screen`;
|
||||||
|
return skip ? `${name}.skip.shielded` : `${name}.shielded`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${screen}.screen`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the alert options for the alert.
|
||||||
|
*/
|
||||||
|
buildAlertOptions = async (server: string, translations: Record<string, string>, callback?: (value: boolean) => void) => {
|
||||||
|
const buttons: AlertButton[] = [];
|
||||||
|
const hasSessionToServer = await getServerCredentials(server);
|
||||||
|
if (server && hasSessionToServer) {
|
||||||
|
buttons.push({
|
||||||
|
text: translations[messages.logout.id],
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: async () => {
|
||||||
|
await logout(server, undefined);
|
||||||
|
callback?.(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const otherServers = Object.keys(this.serverConfig).filter((s) => s !== server);
|
||||||
|
if (otherServers.length > 0) {
|
||||||
|
if (otherServers.length === 1 && otherServers[0] === this.activeServer) {
|
||||||
|
buttons.push({
|
||||||
|
text: translations[messages.ok.id],
|
||||||
|
style: 'cancel',
|
||||||
|
onPress: () => {
|
||||||
|
callback?.(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
buttons.push({
|
||||||
|
text: translations[messages.switchServer.id],
|
||||||
|
style: 'cancel',
|
||||||
|
onPress: () => {
|
||||||
|
this.goToPreviousServer(otherServers);
|
||||||
|
callback?.(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buttons.length === 0) {
|
||||||
|
buttons.push({
|
||||||
|
text: translations[messages.exit.id],
|
||||||
|
style: 'destructive',
|
||||||
|
onPress: () => {
|
||||||
|
Emm.exitApp();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
};
|
||||||
|
|
||||||
|
showDeviceNotTrustedAlert = async (server: string, siteName: string | undefined, translations: Record<string, string>) => {
|
||||||
|
const buttons = await this.buildAlertOptions(server, translations);
|
||||||
|
const securedBy = siteName || this.getServerConfig(server)?.siteName || 'Mattermost';
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
translations[messages.blocked_by.id].replace('{vendor}', securedBy),
|
||||||
|
translations[messages.jailbreak.id].
|
||||||
|
replace('{vendor}', securedBy),
|
||||||
|
buttons,
|
||||||
|
{cancelable: false},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows an alert when the device does not have biometrics or passcode set.
|
||||||
|
*/
|
||||||
|
showNotSecuredAlert = async (server: string, siteName: string | undefined, translations: Record<string, string>) => {
|
||||||
|
const buttons: AlertButton[] = [];
|
||||||
|
const config = this.serverConfig[server];
|
||||||
|
const securedBy = siteName || config?.siteName || 'Mattermost';
|
||||||
|
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
buttons.push({
|
||||||
|
text: translations[messages.androidSettings.id],
|
||||||
|
onPress: () => {
|
||||||
|
Emm.openSecuritySettings();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const alertButtons = await this.buildAlertOptions(server, translations);
|
||||||
|
buttons.push(...alertButtons);
|
||||||
|
|
||||||
|
let message;
|
||||||
|
if (config?.siteName || siteName) {
|
||||||
|
const key = Platform.select({ios: messages.not_secured_vendor_ios.id, default: messages.not_secured_vendor_android.id});
|
||||||
|
message = translations[key].replace('{vendor}', securedBy);
|
||||||
|
} else {
|
||||||
|
const key = Platform.select({ios: messages.not_secured_ios.id, default: messages.not_secured_android.id});
|
||||||
|
message = translations[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
translations[messages.blocked_by.id].replace('{vendor}', securedBy),
|
||||||
|
message,
|
||||||
|
buttons,
|
||||||
|
{cancelable: false},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows an alert when biometric authentication fails.
|
||||||
|
*/
|
||||||
|
showBiometricFailureAlert = async (server: string, blurOnAuthenticate: boolean, siteName: string | undefined, translations: Record<string, string>) => {
|
||||||
|
const buttons = await this.buildAlertOptions(server, translations, () => {
|
||||||
|
if (blurOnAuthenticate) {
|
||||||
|
Emm.removeBlurEffect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const securedBy = siteName || this.getServerConfig(server)?.siteName || 'Mattermost';
|
||||||
|
|
||||||
|
Alert.alert(
|
||||||
|
translations[messages.blocked_by.id].replace('{vendor}', securedBy),
|
||||||
|
translations[messages.biometric_failed.id],
|
||||||
|
buttons,
|
||||||
|
{cancelable: false},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const SecurityManager = new SecurityManagerSingleton();
|
||||||
|
export default SecurityManager;
|
||||||
|
|
@ -11,6 +11,7 @@ import {getAllServerCredentials, removeServerCredentials} from '@init/credential
|
||||||
import {relaunchApp} from '@init/launch';
|
import {relaunchApp} from '@init/launch';
|
||||||
import PushNotifications from '@init/push_notifications';
|
import PushNotifications from '@init/push_notifications';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
||||||
import {isMainActivity} from '@utils/helpers';
|
import {isMainActivity} from '@utils/helpers';
|
||||||
|
|
@ -37,6 +38,7 @@ jest.mock('@init/credentials');
|
||||||
jest.mock('@init/launch');
|
jest.mock('@init/launch');
|
||||||
jest.mock('@init/push_notifications');
|
jest.mock('@init/push_notifications');
|
||||||
jest.mock('@managers/network_manager');
|
jest.mock('@managers/network_manager');
|
||||||
|
jest.mock('@managers/security_manager');
|
||||||
jest.mock('@managers/websocket_manager');
|
jest.mock('@managers/websocket_manager');
|
||||||
jest.mock('@queries/app/servers');
|
jest.mock('@queries/app/servers');
|
||||||
jest.mock('@queries/servers/user');
|
jest.mock('@queries/servers/user');
|
||||||
|
|
@ -107,6 +109,7 @@ describe('SessionManager', () => {
|
||||||
expect(PushNotifications.removeServerNotifications).toHaveBeenCalledWith(mockServerUrl);
|
expect(PushNotifications.removeServerNotifications).toHaveBeenCalledWith(mockServerUrl);
|
||||||
expect(NetworkManager.invalidateClient).toHaveBeenCalledWith(mockServerUrl);
|
expect(NetworkManager.invalidateClient).toHaveBeenCalledWith(mockServerUrl);
|
||||||
expect(WebsocketManager.invalidateClient).toHaveBeenCalledWith(mockServerUrl);
|
expect(WebsocketManager.invalidateClient).toHaveBeenCalledWith(mockServerUrl);
|
||||||
|
expect(SecurityManager.removeServer).toHaveBeenCalledWith(mockServerUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle session expiration', async () => {
|
it('should handle session expiration', async () => {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {getAllServerCredentials, removeServerCredentials} from '@init/credential
|
||||||
import {relaunchApp} from '@init/launch';
|
import {relaunchApp} from '@init/launch';
|
||||||
import PushNotifications from '@init/push_notifications';
|
import PushNotifications from '@init/push_notifications';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
import {getAllServers, getServerDisplayName} from '@queries/app/servers';
|
import {getAllServers, getServerDisplayName} from '@queries/app/servers';
|
||||||
import {getCurrentUser} from '@queries/servers/user';
|
import {getCurrentUser} from '@queries/servers/user';
|
||||||
|
|
@ -114,6 +115,7 @@ export class SessionManagerSingleton {
|
||||||
cancelSessionNotification(serverUrl);
|
cancelSessionNotification(serverUrl);
|
||||||
await removeServerCredentials(serverUrl);
|
await removeServerCredentials(serverUrl);
|
||||||
PushNotifications.removeServerNotifications(serverUrl);
|
PushNotifications.removeServerNotifications(serverUrl);
|
||||||
|
SecurityManager.removeServer(serverUrl);
|
||||||
|
|
||||||
NetworkManager.invalidateClient(serverUrl);
|
NetworkManager.invalidateClient(serverUrl);
|
||||||
WebsocketManager.invalidateClient(serverUrl);
|
WebsocketManager.invalidateClient(serverUrl);
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ import {useTheme} from '@context/theme';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
import WebsocketManager from '@managers/websocket_manager';
|
||||||
import {
|
import {
|
||||||
allOrientations,
|
allOrientations,
|
||||||
|
|
@ -736,7 +737,10 @@ const CallScreen = ({
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={style.wrapper}>
|
<SafeAreaView
|
||||||
|
style={style.wrapper}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
<StatusBar barStyle={'light-content'}/>
|
<StatusBar barStyle={'light-content'}/>
|
||||||
<View style={style.container}>
|
<View style={style.container}>
|
||||||
{!isLandscape && header}
|
{!isLandscape && header}
|
||||||
|
|
|
||||||
|
|
@ -137,12 +137,12 @@ export const getCommonSystemValues = async (serverDatabase: Database) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fromModelToClientConfig = (list: ConfigModel[]) => {
|
const fromModelToClientConfig = <T = ClientConfig>(list: ConfigModel[]) => {
|
||||||
const config: {[key: string]: any} = {};
|
const config: {[key: string]: any} = {};
|
||||||
list.forEach((v) => {
|
list.forEach((v) => {
|
||||||
config[v.id] = v.value;
|
config[v.id] = v.value;
|
||||||
});
|
});
|
||||||
return config as ClientConfig;
|
return config as T;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getConfig = async (database: Database) => {
|
export const getConfig = async (database: Database) => {
|
||||||
|
|
@ -150,6 +150,12 @@ export const getConfig = async (database: Database) => {
|
||||||
return fromModelToClientConfig(configList);
|
return fromModelToClientConfig(configList);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getSecurityConfig = async (database: Database) => {
|
||||||
|
const configList = await database.get<ConfigModel>(CONFIG).query(
|
||||||
|
Q.where('id', Q.oneOf(['MobileEnableBiometrics', 'MobileJailbreakProtection', 'MobilePreventScreenCapture', 'SiteName']))).fetch();
|
||||||
|
return fromModelToClientConfig<SecurityClientConfig>(configList);
|
||||||
|
};
|
||||||
|
|
||||||
export const queryConfigValue = (database: Database, key: keyof ClientConfig) => {
|
export const queryConfigValue = (database: Database, key: keyof ClientConfig) => {
|
||||||
return database.get<ConfigModel>(CONFIG).query(Q.where('id', Q.eq(key)));
|
return database.get<ConfigModel>(CONFIG).query(Q.where('id', Q.eq(key)));
|
||||||
};
|
};
|
||||||
|
|
@ -205,7 +211,7 @@ export const getIsDataRetentionEnabled = async (database: Database) => {
|
||||||
|
|
||||||
export const observeConfig = (database: Database): Observable<ClientConfig | undefined> => {
|
export const observeConfig = (database: Database): Observable<ClientConfig | undefined> => {
|
||||||
return database.get<ConfigModel>(CONFIG).query().observeWithColumns(['value']).pipe(
|
return database.get<ConfigModel>(CONFIG).query().observeWithColumns(['value']).pipe(
|
||||||
switchMap((result) => of$(fromModelToClientConfig(result))),
|
switchMap((result) => of$(fromModelToClientConfig<ClientConfig>(result))),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useDidUpdate from '@hooks/did_update';
|
import useDidUpdate from '@hooks/did_update';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {filterEmptyOptions} from '@utils/apps';
|
import {filterEmptyOptions} from '@utils/apps';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
||||||
|
|
@ -385,6 +386,7 @@ function AppsFormComponent({
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
testID='interactive_dialog.screen'
|
testID='interactive_dialog.screen'
|
||||||
style={style.container}
|
style={style.container}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
ref={scrollView}
|
ref={scrollView}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {hapticFeedback} from '@utils/general';
|
import {hapticFeedback} from '@utils/general';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
@ -198,11 +199,14 @@ const BottomSheet = ({
|
||||||
if (isTablet) {
|
if (isTablet) {
|
||||||
const FooterComponent = footerComponent;
|
const FooterComponent = footerComponent;
|
||||||
return (
|
return (
|
||||||
<>
|
<View
|
||||||
|
style={styles.view}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
<View style={styles.separator}/>
|
<View style={styles.separator}/>
|
||||||
{renderContainerContent()}
|
{renderContainerContent()}
|
||||||
{FooterComponent && (<FooterComponent/>)}
|
{FooterComponent && (<FooterComponent/>)}
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal, goToScreen, setButtons} from '@screens/navigation';
|
import {dismissModal, goToScreen, setButtons} from '@screens/navigation';
|
||||||
import {alertErrorWithFallback} from '@utils/draft';
|
import {alertErrorWithFallback} from '@utils/draft';
|
||||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||||
|
|
@ -231,7 +232,10 @@ export default function BrowseChannels(props: Props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={style.container}>
|
<SafeAreaView
|
||||||
|
style={style.container}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
{content}
|
{content}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {useChannelSwitch} from '@hooks/channel_switch';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||||
import {useTeamSwitch} from '@hooks/team_switch';
|
import {useTeamSwitch} from '@hooks/team_switch';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import EphemeralStore from '@store/ephemeral_store';
|
||||||
|
|
||||||
|
|
@ -116,6 +117,7 @@ const Channel = ({
|
||||||
edges={edges}
|
edges={edges}
|
||||||
testID='channel.screen'
|
testID='channel.screen'
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
|
nativeID={componentId ? SecurityManager.getShieldScreenId(componentId) : undefined}
|
||||||
>
|
>
|
||||||
<ChannelHeader
|
<ChannelHeader
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useKeyboardOverlap} from '@hooks/device';
|
import {useKeyboardOverlap} from '@hooks/device';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {alertErrorWithFallback} from '@utils/draft';
|
import {alertErrorWithFallback} from '@utils/draft';
|
||||||
import {mergeNavigationOptions} from '@utils/navigation';
|
import {mergeNavigationOptions} from '@utils/navigation';
|
||||||
|
|
@ -255,6 +256,7 @@ export default function ChannelAddMembers({
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
ref={mainView}
|
ref={mainView}
|
||||||
edges={['top', 'left', 'right']}
|
edges={['top', 'left', 'right']}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<View style={style.searchBar}>
|
<View style={style.searchBar}>
|
||||||
<Search
|
<Search
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {getFullErrorMessage} from '@utils/errors';
|
import {getFullErrorMessage} from '@utils/errors';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
@ -269,6 +270,7 @@ const ChannelBookmarkAddOrEdit = ({
|
||||||
edges={edges}
|
edges={edges}
|
||||||
style={styles.content}
|
style={styles.content}
|
||||||
testID='channel_bookmark.screen'
|
testID='channel_bookmark.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
{type === 'link' &&
|
{type === 'link' &&
|
||||||
<BookmarkLink
|
<BookmarkLink
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {General} from '@constants';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import {type FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
import {type FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
||||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||||
|
|
@ -135,48 +136,53 @@ function ChannelFiles({
|
||||||
const fileChannels = useMemo(() => [channel], [channel]);
|
const fileChannels = useMemo(() => [channel], [channel]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<View
|
||||||
edges={edges}
|
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID={`${TEST_ID}.screen`}
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<View style={styles.searchBar}>
|
<SafeAreaView
|
||||||
<Search
|
edges={edges}
|
||||||
testID={`${TEST_ID}.search_bar`}
|
style={styles.flex}
|
||||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
testID={`${TEST_ID}.screen`}
|
||||||
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
>
|
||||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
<View style={styles.searchBar}>
|
||||||
onChangeText={onTextChange}
|
<Search
|
||||||
onCancel={clearSearch}
|
testID={`${TEST_ID}.search_bar`}
|
||||||
autoCapitalize='none'
|
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||||
value={term}
|
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||||
|
onChangeText={onTextChange}
|
||||||
|
onCancel={clearSearch}
|
||||||
|
autoCapitalize='none'
|
||||||
|
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||||
|
value={term}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<Header
|
||||||
|
onFilterChanged={handleFilterChange}
|
||||||
|
selectedFilter={filter}
|
||||||
/>
|
/>
|
||||||
</View>
|
{loading &&
|
||||||
<Header
|
|
||||||
onFilterChanged={handleFilterChange}
|
|
||||||
selectedFilter={filter}
|
|
||||||
/>
|
|
||||||
{loading &&
|
|
||||||
<Loading
|
<Loading
|
||||||
color={theme.buttonBg}
|
color={theme.buttonBg}
|
||||||
size='large'
|
size='large'
|
||||||
containerStyle={styles.loading}
|
containerStyle={styles.loading}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
{!loading &&
|
{!loading &&
|
||||||
<FileResults
|
<FileResults
|
||||||
canDownloadFiles={canDownloadFiles}
|
canDownloadFiles={canDownloadFiles}
|
||||||
fileChannels={fileChannels}
|
fileChannels={fileChannels}
|
||||||
fileInfos={fileInfos}
|
fileInfos={fileInfos}
|
||||||
paddingTop={styles.noPaddingTop}
|
paddingTop={styles.noPaddingTop}
|
||||||
publicLinkEnabled={publicLinkEnabled}
|
publicLinkEnabled={publicLinkEnabled}
|
||||||
searchValue={term}
|
searchValue={term}
|
||||||
isChannelFiles={true}
|
isChannelFiles={true}
|
||||||
isFilterEnabled={filter !== FileFilters.ALL}
|
isFilterEnabled={filter !== FileFilters.ALL}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
|
@ -96,73 +97,78 @@ const ChannelInfo = ({
|
||||||
const convertGMOptionAvailable = isConvertGMFeatureAvailable && type === General.GM_CHANNEL && !isGuestUser;
|
const convertGMOptionAvailable = isConvertGMFeatureAvailable && type === General.GM_CHANNEL && !isGuestUser;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<View
|
||||||
edges={edges}
|
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID='channel_info.screen'
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<SafeAreaView
|
||||||
bounces={true}
|
edges={edges}
|
||||||
alwaysBounceVertical={false}
|
style={styles.flex}
|
||||||
contentContainerStyle={styles.content}
|
testID='channel_info.screen'
|
||||||
testID='channel_info.scroll_view'
|
|
||||||
>
|
>
|
||||||
<Title
|
<ScrollView
|
||||||
channelId={channelId}
|
bounces={true}
|
||||||
type={type}
|
alwaysBounceVertical={false}
|
||||||
/>
|
contentContainerStyle={styles.content}
|
||||||
{isBookmarksEnabled &&
|
testID='channel_info.scroll_view'
|
||||||
<ChannelBookmarks
|
>
|
||||||
|
<Title
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
canAddBookmarks={canAddBookmarks}
|
type={type}
|
||||||
showInInfo={true}
|
|
||||||
/>
|
/>
|
||||||
}
|
{isBookmarksEnabled &&
|
||||||
<ChannelActions
|
<ChannelBookmarks
|
||||||
channelId={channelId}
|
|
||||||
inModal={true}
|
|
||||||
dismissChannelInfo={onPressed}
|
|
||||||
callsEnabled={callsAvailable}
|
|
||||||
testID='channel_info.channel_actions'
|
|
||||||
/>
|
|
||||||
<Extra channelId={channelId}/>
|
|
||||||
<View style={styles.separator}/>
|
|
||||||
<Options
|
|
||||||
channelId={channelId}
|
|
||||||
type={type}
|
|
||||||
callsEnabled={callsAvailable}
|
|
||||||
canManageMembers={canManageMembers}
|
|
||||||
isCRTEnabled={isCRTEnabled}
|
|
||||||
canManageSettings={canManageSettings}
|
|
||||||
/>
|
|
||||||
<View style={styles.separator}/>
|
|
||||||
{convertGMOptionAvailable &&
|
|
||||||
<>
|
|
||||||
<ConvertToChannelLabel channelId={channelId}/>
|
|
||||||
<View style={styles.separator}/>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
{canEnableDisableCalls &&
|
|
||||||
<>
|
|
||||||
<ChannelInfoEnableCalls
|
|
||||||
channelId={channelId}
|
channelId={channelId}
|
||||||
enabled={isCallsEnabledInChannel}
|
canAddBookmarks={canAddBookmarks}
|
||||||
|
showInInfo={true}
|
||||||
/>
|
/>
|
||||||
|
}
|
||||||
|
<ChannelActions
|
||||||
|
channelId={channelId}
|
||||||
|
inModal={true}
|
||||||
|
dismissChannelInfo={onPressed}
|
||||||
|
callsEnabled={callsAvailable}
|
||||||
|
testID='channel_info.channel_actions'
|
||||||
|
/>
|
||||||
|
<Extra channelId={channelId}/>
|
||||||
|
<View style={styles.separator}/>
|
||||||
|
<Options
|
||||||
|
channelId={channelId}
|
||||||
|
type={type}
|
||||||
|
callsEnabled={callsAvailable}
|
||||||
|
canManageMembers={canManageMembers}
|
||||||
|
isCRTEnabled={isCRTEnabled}
|
||||||
|
canManageSettings={canManageSettings}
|
||||||
|
/>
|
||||||
|
<View style={styles.separator}/>
|
||||||
|
{convertGMOptionAvailable &&
|
||||||
|
<>
|
||||||
|
<ConvertToChannelLabel channelId={channelId}/>
|
||||||
<View style={styles.separator}/>
|
<View style={styles.separator}/>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
<ChannelInfoAppBindings
|
{canEnableDisableCalls &&
|
||||||
channelId={channelId}
|
<>
|
||||||
serverUrl={serverUrl}
|
<ChannelInfoEnableCalls
|
||||||
dismissChannelInfo={onPressed}
|
channelId={channelId}
|
||||||
/>
|
enabled={isCallsEnabledInChannel}
|
||||||
<DestructiveOptions
|
/>
|
||||||
channelId={channelId}
|
<View style={styles.separator}/>
|
||||||
componentId={componentId}
|
</>
|
||||||
type={type}
|
}
|
||||||
/>
|
<ChannelInfoAppBindings
|
||||||
</ScrollView>
|
channelId={channelId}
|
||||||
</SafeAreaView>
|
serverUrl={serverUrl}
|
||||||
|
dismissChannelInfo={onPressed}
|
||||||
|
/>
|
||||||
|
<DestructiveOptions
|
||||||
|
channelId={channelId}
|
||||||
|
componentId={componentId}
|
||||||
|
type={type}
|
||||||
|
/>
|
||||||
|
</ScrollView>
|
||||||
|
</SafeAreaView>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {SNACK_BAR_TYPE} from '@constants/snack_bar';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||||
import {showSnackBar} from '@utils/snack_bar';
|
import {showSnackBar} from '@utils/snack_bar';
|
||||||
|
|
||||||
|
|
@ -64,6 +65,7 @@ const Code = ({code, componentId, language, textStyle}: Props) => {
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
edges={edges}
|
edges={edges}
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<SyntaxHiglight
|
<SyntaxHiglight
|
||||||
code={code}
|
code={code}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import AutocompleteSelector from '@components/autocomplete_selector';
|
||||||
import {Preferences} from '@constants';
|
import {Preferences} from '@constants';
|
||||||
import {CustomThemeProvider} from '@context/theme';
|
import {CustomThemeProvider} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
|
|
||||||
import ButtonComponentLibrary from './button.cl';
|
import ButtonComponentLibrary from './button.cl';
|
||||||
|
|
@ -111,7 +112,10 @@ const ComponentLibrary = ({componentId}: Props) => {
|
||||||
|
|
||||||
const SelectedComponent = componentMap[selectedComponent];
|
const SelectedComponent = componentMap[selectedComponent];
|
||||||
return (
|
return (
|
||||||
<ScrollView style={{margin: 10}}>
|
<ScrollView
|
||||||
|
style={{margin: 10}}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
<AutocompleteSelector
|
<AutocompleteSelector
|
||||||
testID='selectedComponent'
|
testID='selectedComponent'
|
||||||
label='Component'
|
label='Component'
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,24 @@
|
||||||
|
|
||||||
import React, {useEffect, useRef, useState} from 'react';
|
import React, {useEffect, useRef, useState} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
import {fetchChannelMemberships, fetchGroupMessageMembersCommonTeams} from '@actions/remote/channel';
|
import {fetchChannelMemberships, fetchGroupMessageMembersCommonTeams} from '@actions/remote/channel';
|
||||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||||
import Loading from '@components/loading';
|
import Loading from '@components/loading';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
||||||
import ConvertGMToChannelForm from './convert_gm_to_channel_form';
|
import ConvertGMToChannelForm from './convert_gm_to_channel_form';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
channelId: string;
|
channelId: string;
|
||||||
|
componentId: AvailableScreens;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,11 +66,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
gap: 24,
|
gap: 24,
|
||||||
},
|
},
|
||||||
|
flex: {flex: 1},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const ConvertGMToChannel = ({
|
const ConvertGMToChannel = ({
|
||||||
channelId,
|
channelId,
|
||||||
|
componentId,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
@ -131,8 +138,9 @@ const ConvertGMToChannel = ({
|
||||||
|
|
||||||
const showLoader = !loadingAnimationTimeout || !commonTeamsFetched || !channelMembersFetched;
|
const showLoader = !loadingAnimationTimeout || !commonTeamsFetched || !channelMembersFetched;
|
||||||
|
|
||||||
|
let component;
|
||||||
if (showLoader) {
|
if (showLoader) {
|
||||||
return (
|
component = (
|
||||||
<Loading
|
<Loading
|
||||||
containerStyle={styles.loadingContainer}
|
containerStyle={styles.loadingContainer}
|
||||||
size='large'
|
size='large'
|
||||||
|
|
@ -141,14 +149,23 @@ const ConvertGMToChannel = ({
|
||||||
footerTextStyles={styles.text}
|
footerTextStyles={styles.text}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
component = (
|
||||||
|
<ConvertGMToChannelForm
|
||||||
|
commonTeams={commonTeams}
|
||||||
|
profiles={profiles}
|
||||||
|
channelId={channelId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ConvertGMToChannelForm
|
<View
|
||||||
commonTeams={commonTeams}
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
profiles={profiles}
|
style={styles.flex}
|
||||||
channelId={channelId}
|
>
|
||||||
/>
|
{component}
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@ import {StyleSheet, View} from 'react-native';
|
||||||
import SearchBar from '@components/search';
|
import SearchBar from '@components/search';
|
||||||
import TeamList from '@components/team_list';
|
import TeamList from '@components/team_list';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
import {usePreventDoubleTap} from '@hooks/utils';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
|
||||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
padding: 12,
|
padding: 12,
|
||||||
|
|
@ -22,11 +25,12 @@ const styles = StyleSheet.create({
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
componentId: AvailableScreens;
|
||||||
teams: Team[];
|
teams: Team[];
|
||||||
selectTeam: (teamId: string) => void;
|
selectTeam: (teamId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TeamSelectorList = ({teams, selectTeam}: Props) => {
|
const TeamSelectorList = ({componentId, teams, selectTeam}: Props) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [filteredTeams, setFilteredTeam] = useState(teams);
|
const [filteredTeams, setFilteredTeam] = useState(teams);
|
||||||
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
||||||
|
|
@ -39,13 +43,16 @@ const TeamSelectorList = ({teams, selectTeam}: Props) => {
|
||||||
}
|
}
|
||||||
}, 200), [teams]);
|
}, 200), [teams]);
|
||||||
|
|
||||||
const handleOnPress = useCallback(preventDoubleTap((teamId: string) => {
|
const handleOnPress = usePreventDoubleTap(useCallback((teamId: string) => {
|
||||||
selectTeam(teamId);
|
selectTeam(teamId);
|
||||||
popTopScreen();
|
popTopScreen();
|
||||||
}), []);
|
}, []));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useKeyboardOverlap} from '@hooks/device';
|
import {useKeyboardOverlap} from '@hooks/device';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal, setButtons} from '@screens/navigation';
|
import {dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {alertErrorWithFallback} from '@utils/draft';
|
import {alertErrorWithFallback} from '@utils/draft';
|
||||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
@ -298,6 +299,7 @@ export default function CreateDirectMessage({
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={style.container}
|
style={style.container}
|
||||||
testID='create_direct_message.screen'
|
testID='create_direct_message.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
ref={mainView}
|
ref={mainView}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react';
|
import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {Keyboard} from 'react-native';
|
import {Keyboard, StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
import {createChannel, patchChannel as handlePatchChannel, switchToChannelById} from '@actions/remote/channel';
|
import {createChannel, patchChannel as handlePatchChannel, switchToChannelById} from '@actions/remote/channel';
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
|
|
@ -13,6 +13,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {buildNavigationButton, dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
import {buildNavigationButton, dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
||||||
import {validateDisplayName} from '@utils/channel';
|
import {validateDisplayName} from '@utils/channel';
|
||||||
|
|
||||||
|
|
@ -68,6 +69,12 @@ const makeCloseButton = (icon: ImageResource) => {
|
||||||
return buildNavigationButton(CLOSE_BUTTON_ID, 'close.create_or_edit_channel.button', icon);
|
return buildNavigationButton(CLOSE_BUTTON_ID, 'close.create_or_edit_channel.button', icon);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const CreateOrEditChannel = ({
|
const CreateOrEditChannel = ({
|
||||||
componentId,
|
componentId,
|
||||||
channel,
|
channel,
|
||||||
|
|
@ -233,21 +240,26 @@ const CreateOrEditChannel = ({
|
||||||
useAndroidHardwareBackHandler(componentId, handleClose);
|
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ChannelInfoForm
|
<View
|
||||||
error={appState.error}
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
saving={appState.saving}
|
style={styles.container}
|
||||||
channelType={channel?.type}
|
>
|
||||||
editing={editing}
|
<ChannelInfoForm
|
||||||
onTypeChange={setType}
|
error={appState.error}
|
||||||
type={type}
|
saving={appState.saving}
|
||||||
displayName={displayName}
|
channelType={channel?.type}
|
||||||
onDisplayNameChange={setDisplayName}
|
editing={editing}
|
||||||
header={header}
|
onTypeChange={setType}
|
||||||
headerOnly={headerOnly}
|
type={type}
|
||||||
onHeaderChange={setHeader}
|
displayName={displayName}
|
||||||
purpose={purpose}
|
onDisplayNameChange={setDisplayName}
|
||||||
onPurposeChange={setPurpose}
|
header={header}
|
||||||
/>
|
headerOnly={headerOnly}
|
||||||
|
onHeaderChange={setHeader}
|
||||||
|
purpose={purpose}
|
||||||
|
onPurposeChange={setPurpose}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal, goToScreen, openAsBottomSheet, showModal} from '@screens/navigation';
|
import {dismissModal, goToScreen, openAsBottomSheet, showModal} from '@screens/navigation';
|
||||||
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
|
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
|
||||||
import {logDebug} from '@utils/log';
|
import {logDebug} from '@utils/log';
|
||||||
|
|
@ -75,6 +76,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
||||||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||||
borderTopWidth: 1,
|
borderTopWidth: 1,
|
||||||
},
|
},
|
||||||
|
flex: {flex: 1},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -345,7 +347,10 @@ const CustomStatus = ({
|
||||||
}, [isBtnEnabled]);
|
}, [isBtnEnabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View
|
||||||
|
style={style.flex}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
{isTablet &&
|
{isTablet &&
|
||||||
<TabletTitle
|
<TabletTitle
|
||||||
action={intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'})}
|
action={intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'})}
|
||||||
|
|
@ -411,7 +416,7 @@ const CustomStatus = ({
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
} from 'react-native-navigation';
|
} from 'react-native-navigation';
|
||||||
|
|
||||||
import {CustomStatusDurationEnum} from '@constants/custom_status';
|
import {CustomStatusDurationEnum} from '@constants/custom_status';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {observeCurrentUser} from '@queries/servers/user';
|
import {observeCurrentUser} from '@queries/servers/user';
|
||||||
import {dismissModal, popTopScreen} from '@screens/navigation';
|
import {dismissModal, popTopScreen} from '@screens/navigation';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
|
|
@ -180,13 +181,14 @@ class ClearAfterModal extends NavigationComponent<Props, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {currentUser, theme} = this.props;
|
const {componentId, currentUser, theme} = this.props;
|
||||||
const style = getStyleSheet(theme);
|
const style = getStyleSheet(theme);
|
||||||
const {duration, expiresAt, showExpiryTime} = this.state;
|
const {duration, expiresAt, showExpiryTime} = this.state;
|
||||||
return (
|
return (
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={style.container}
|
style={style.container}
|
||||||
testID='custom_status_clear_after.screen'
|
testID='custom_status_clear_after.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<KeyboardAwareScrollView bounces={false}>
|
<KeyboardAwareScrollView bounces={false}>
|
||||||
<View style={style.scrollView}>
|
<View style={style.scrollView}>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {useKeyboardOverlap} from '@hooks/device';
|
||||||
import useDidUpdate from '@hooks/did_update';
|
import useDidUpdate from '@hooks/did_update';
|
||||||
import {useInputPropagation} from '@hooks/input';
|
import {useInputPropagation} from '@hooks/input';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import PostError from '@screens/edit_post/post_error';
|
import PostError from '@screens/edit_post/post_error';
|
||||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
|
@ -207,7 +208,10 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
||||||
|
|
||||||
if (isUpdating) {
|
if (isUpdating) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.loader}>
|
<View
|
||||||
|
style={styles.loader}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
<Loading color={theme.buttonBg}/>
|
<Loading color={theme.buttonBg}/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
@ -219,6 +223,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
||||||
testID='edit_post.screen'
|
testID='edit_post.screen'
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={styles.body}
|
style={styles.body}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
import {dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
|
|
||||||
|
|
@ -295,7 +296,10 @@ const EditProfile = ({
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View
|
||||||
|
style={styles.flex}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
{isTablet &&
|
{isTablet &&
|
||||||
<TabletTitle
|
<TabletTitle
|
||||||
action={buttonText}
|
action={buttonText}
|
||||||
|
|
@ -312,7 +316,7 @@ const EditProfile = ({
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {getServerByDisplayName} from '@queries/app/servers';
|
import {getServerByDisplayName} from '@queries/app/servers';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
|
|
@ -93,7 +94,10 @@ const EditServer = ({closeButtonId, componentId, server, theme}: ServerProps) =>
|
||||||
useAndroidHardwareBackHandler(componentId, close);
|
useAndroidHardwareBackHandler(componentId, close);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.flex}>
|
<View
|
||||||
|
style={styles.flex}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
<Background theme={theme}/>
|
<Background theme={theme}/>
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
key={'server_content'}
|
key={'server_content'}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {searchCustomEmojis} from '@actions/remote/custom_emoji';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {debounce} from '@helpers/api/general';
|
import {debounce} from '@helpers/api/general';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {getKeyboardAppearanceFromTheme} from '@utils/theme';
|
import {getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||||
|
|
||||||
import EmojiFiltered from './filtered';
|
import EmojiFiltered from './filtered';
|
||||||
|
|
@ -15,6 +16,7 @@ import PickerHeader from './header';
|
||||||
import EmojiSections from './sections';
|
import EmojiSections from './sections';
|
||||||
|
|
||||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||||
|
|
||||||
|
|
@ -82,6 +84,7 @@ const Picker = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmojiPress
|
||||||
<View
|
<View
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID={`${testID}.screen`}
|
testID={`${testID}.screen`}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(testID as AvailableScreens)}
|
||||||
>
|
>
|
||||||
<View style={styles.searchBar}>
|
<View style={styles.searchBar}>
|
||||||
<PickerHeader
|
<PickerHeader
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useKeyboardOverlap} from '@hooks/device';
|
import {useKeyboardOverlap} from '@hooks/device';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
@ -87,6 +88,7 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
|
||||||
<View
|
<View
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
testID='find_channels.screen'
|
testID='find_channels.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<SearchBar
|
<SearchBar
|
||||||
autoCapitalize='none'
|
autoCapitalize='none'
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import FormattedText from '@components/formatted_text';
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useAvoidKeyboard} from '@hooks/device';
|
import {useAvoidKeyboard} from '@hooks/device';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
import {isEmail} from '@utils/helpers';
|
import {isEmail} from '@utils/helpers';
|
||||||
|
|
@ -141,6 +142,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
|
||||||
<View
|
<View
|
||||||
style={styles.successContainer}
|
style={styles.successContainer}
|
||||||
testID={'password_send.link.sent'}
|
testID={'password_send.link.sent'}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
>
|
>
|
||||||
<Inbox/>
|
<Inbox/>
|
||||||
<FormattedText
|
<FormattedText
|
||||||
|
|
@ -184,6 +186,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
|
||||||
ref={keyboardAwareRef}
|
ref={keyboardAwareRef}
|
||||||
scrollToOverflowEnabled={true}
|
scrollToOverflowEnabled={true}
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
style={styles.centered}
|
style={styles.centered}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@
|
||||||
|
|
||||||
import RNUtils from '@mattermost/rnutils';
|
import RNUtils from '@mattermost/rnutils';
|
||||||
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||||
import {Platform} from 'react-native';
|
import {Platform, StyleSheet, View} from 'react-native';
|
||||||
|
|
||||||
import {CaptionsEnabledContext} from '@calls/context';
|
import {CaptionsEnabledContext} from '@calls/context';
|
||||||
import {hasCaptions} from '@calls/utils';
|
import {hasCaptions} from '@calls/utils';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useIsTablet, useWindowDimensions} from '@hooks/device';
|
import {useIsTablet, useWindowDimensions} from '@hooks/device';
|
||||||
import {useGalleryControls} from '@hooks/gallery';
|
import {useGalleryControls} from '@hooks/gallery';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissOverlay, setScreensOrientation} from '@screens/navigation';
|
import {dismissOverlay, setScreensOrientation} from '@screens/navigation';
|
||||||
import {freezeOtherScreens} from '@utils/gallery';
|
import {freezeOtherScreens} from '@utils/gallery';
|
||||||
|
|
||||||
|
|
@ -28,6 +29,10 @@ type Props = {
|
||||||
items: GalleryItemType[];
|
items: GalleryItemType[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
flex: {flex: 1},
|
||||||
|
});
|
||||||
|
|
||||||
const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialIndex, items}: Props) => {
|
const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialIndex, items}: Props) => {
|
||||||
const dim = useWindowDimensions();
|
const dim = useWindowDimensions();
|
||||||
const isTablet = useIsTablet();
|
const isTablet = useIsTablet();
|
||||||
|
|
@ -84,30 +89,35 @@ const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialInde
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CaptionsEnabledContext.Provider value={captionsEnabled}>
|
<CaptionsEnabledContext.Provider value={captionsEnabled}>
|
||||||
<Header
|
<View
|
||||||
index={localIndex}
|
style={styles.flex}
|
||||||
onClose={onClose}
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
style={headerStyles}
|
>
|
||||||
total={items.length}
|
<Header
|
||||||
/>
|
index={localIndex}
|
||||||
<Gallery
|
onClose={onClose}
|
||||||
galleryIdentifier={galleryIdentifier}
|
style={headerStyles}
|
||||||
initialIndex={initialIndex}
|
total={items.length}
|
||||||
items={items}
|
/>
|
||||||
onHide={close}
|
<Gallery
|
||||||
onIndexChange={onIndexChange}
|
galleryIdentifier={galleryIdentifier}
|
||||||
onShouldHideControls={setControlsHidden}
|
initialIndex={initialIndex}
|
||||||
ref={galleryRef}
|
items={items}
|
||||||
targetDimensions={dimensions}
|
onHide={close}
|
||||||
/>
|
onIndexChange={onIndexChange}
|
||||||
<Footer
|
onShouldHideControls={setControlsHidden}
|
||||||
hideActions={hideActions}
|
ref={galleryRef}
|
||||||
item={items[localIndex]}
|
targetDimensions={dimensions}
|
||||||
style={footerStyles}
|
/>
|
||||||
hasCaptions={captionsAvailable[localIndex]}
|
<Footer
|
||||||
captionEnabled={captionsEnabled[localIndex]}
|
hideActions={hideActions}
|
||||||
onCaptionsPress={onCaptionsPress}
|
item={items[localIndex]}
|
||||||
/>
|
style={footerStyles}
|
||||||
|
hasCaptions={captionsAvailable[localIndex]}
|
||||||
|
captionEnabled={captionsEnabled[localIndex]}
|
||||||
|
onCaptionsPress={onCaptionsPress}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
</CaptionsEnabledContext.Provider>
|
</CaptionsEnabledContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {Screens} from '@constants';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||||
import {useTeamSwitch} from '@hooks/team_switch';
|
import {useTeamSwitch} from '@hooks/team_switch';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
|
|
||||||
import {popTopScreen} from '../navigation';
|
import {popTopScreen} from '../navigation';
|
||||||
|
|
||||||
|
|
@ -69,6 +70,7 @@ const GlobalDrafts = ({componentId}: Props) => {
|
||||||
mode='margin'
|
mode='margin'
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID='global_drafts.screen'
|
testID='global_drafts.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId || Screens.GLOBAL_DRAFTS)}
|
||||||
>
|
>
|
||||||
<NavigationHeader
|
<NavigationHeader
|
||||||
showBackButton={!isTablet}
|
showBackButton={!isTablet}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||||
import {useTeamSwitch} from '@hooks/team_switch';
|
import {useTeamSwitch} from '@hooks/team_switch';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
|
|
||||||
import ThreadsList from './threads_list';
|
import ThreadsList from './threads_list';
|
||||||
|
|
@ -84,6 +85,7 @@ const GlobalThreads = ({componentId, globalThreadsTab}: Props) => {
|
||||||
mode='margin'
|
mode='margin'
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID='global_threads.screen'
|
testID='global_threads.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId || Screens.GLOBAL_THREADS)}
|
||||||
>
|
>
|
||||||
<NavigationHeader
|
<NavigationHeader
|
||||||
showBackButton={!isTablet}
|
showBackButton={!isTablet}
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,8 @@ import Swipeable from 'react-native-gesture-handler/Swipeable';
|
||||||
import {Navigation} from 'react-native-navigation';
|
import {Navigation} from 'react-native-navigation';
|
||||||
|
|
||||||
import {storeMultiServerTutorial} from '@actions/app/global';
|
import {storeMultiServerTutorial} from '@actions/app/global';
|
||||||
import {doPing} from '@actions/remote/general';
|
import {switchToServer, switchToServerAndLogin} from '@actions/app/server';
|
||||||
import {logout} from '@actions/remote/session';
|
import {logout} from '@actions/remote/session';
|
||||||
import {fetchConfigAndLicense} from '@actions/remote/systems';
|
|
||||||
import CompassIcon from '@components/compass_icon';
|
import CompassIcon from '@components/compass_icon';
|
||||||
import Loading from '@components/loading';
|
import Loading from '@components/loading';
|
||||||
import ServerIcon from '@components/server_icon';
|
import ServerIcon from '@components/server_icon';
|
||||||
|
|
@ -20,14 +19,10 @@ import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left';
|
||||||
import {Events, Screens} from '@constants';
|
import {Events, Screens} from '@constants';
|
||||||
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import DatabaseManager from '@database/manager';
|
|
||||||
import {subscribeServerUnreadAndMentions, type UnreadObserverArgs} from '@database/subscription/unreads';
|
import {subscribeServerUnreadAndMentions, type UnreadObserverArgs} from '@database/subscription/unreads';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
import WebsocketManager from '@managers/websocket_manager';
|
|
||||||
import {getServerByIdentifier} from '@queries/app/servers';
|
|
||||||
import {dismissBottomSheet} from '@screens/navigation';
|
import {dismissBottomSheet} from '@screens/navigation';
|
||||||
import {canReceiveNotifications} from '@utils/push_proxy';
|
import {alertServerLogout, alertServerRemove, editServer} from '@utils/server';
|
||||||
import {alertServerAlreadyConnected, alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
|
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
|
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
|
||||||
|
|
@ -253,29 +248,8 @@ const ServerItem = ({
|
||||||
const handleLogin = useCallback(async () => {
|
const handleLogin = useCallback(async () => {
|
||||||
swipeable.current?.close();
|
swipeable.current?.close();
|
||||||
setSwitching(true);
|
setSwitching(true);
|
||||||
const result = await doPing(server.url, true);
|
await switchToServerAndLogin(server.url, theme, intl, () => setSwitching(false));
|
||||||
if (result.error) {
|
}, [server.url, intl, theme]);
|
||||||
alertServerError(intl, result.error);
|
|
||||||
setSwitching(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await fetchConfigAndLicense(server.url, true);
|
|
||||||
if (data.error) {
|
|
||||||
alertServerError(intl, data.error);
|
|
||||||
setSwitching(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const existingServer = await getServerByIdentifier(data.config!.DiagnosticId);
|
|
||||||
if (existingServer && existingServer.lastActiveAt > 0) {
|
|
||||||
alertServerAlreadyConnected(intl);
|
|
||||||
setSwitching(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canReceiveNotifications(server.url, result.canReceiveNotifications as string, intl);
|
|
||||||
loginToServer(theme, server.url, displayName, data.config!, data.license!);
|
|
||||||
}, [server.url, intl, theme, displayName]);
|
|
||||||
|
|
||||||
const handleDismissTutorial = useCallback(() => {
|
const handleDismissTutorial = useCallback(() => {
|
||||||
swipeable.current?.close();
|
swipeable.current?.close();
|
||||||
|
|
@ -309,14 +283,9 @@ const ServerItem = ({
|
||||||
if (server.lastActiveAt) {
|
if (server.lastActiveAt) {
|
||||||
setSwitching(true);
|
setSwitching(true);
|
||||||
await dismissBottomSheet();
|
await dismissBottomSheet();
|
||||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
|
||||||
DatabaseManager.setActiveServerDatabase(server.url);
|
|
||||||
WebsocketManager.initializeClient(server.url, 'Server Switch');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
await switchToServer(server.url, theme, intl, () => setSwitching(false));
|
||||||
handleLogin();
|
}, [isActive, server.lastActiveAt, server.url, theme, intl]);
|
||||||
}, [isActive, server.lastActiveAt, server.url, handleLogin]);
|
|
||||||
|
|
||||||
const onSwipeableWillOpen = useCallback(() => {
|
const onSwipeableWillOpen = useCallback(() => {
|
||||||
DeviceEventEmitter.emit(Events.SWIPEABLE, server.url);
|
DeviceEventEmitter.emit(Events.SWIPEABLE, server.url);
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,16 @@ import {createBottomTabNavigator, type BottomTabBarProps} from '@react-navigatio
|
||||||
import {NavigationContainer, DefaultTheme} from '@react-navigation/native';
|
import {NavigationContainer, DefaultTheme} from '@react-navigation/native';
|
||||||
import React, {useCallback, useEffect, useMemo} from 'react';
|
import React, {useCallback, useEffect, useMemo} from 'react';
|
||||||
import {useIntl} from 'react-intl';
|
import {useIntl} from 'react-intl';
|
||||||
import {DeviceEventEmitter, Platform} from 'react-native';
|
import {DeviceEventEmitter, Platform, StyleSheet, View} from 'react-native';
|
||||||
import {enableFreeze, enableScreens} from 'react-native-screens';
|
import {enableFreeze, enableScreens} from 'react-native-screens';
|
||||||
|
|
||||||
|
import {initializeSecurityManager} from '@actions/app/server';
|
||||||
import {autoUpdateTimezone} from '@actions/remote/user';
|
import {autoUpdateTimezone} from '@actions/remote/user';
|
||||||
import ServerVersion from '@components/server_version';
|
import ServerVersion from '@components/server_version';
|
||||||
import {Events, Launch, Screens} from '@constants';
|
import {Events, Launch, Screens} from '@constants';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {useAppState} from '@hooks/device';
|
import {useAppState} from '@hooks/device';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {getAllServers} from '@queries/app/servers';
|
import {getAllServers} from '@queries/app/servers';
|
||||||
import {findChannels, popToRoot} from '@screens/navigation';
|
import {findChannels, popToRoot} from '@screens/navigation';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
|
|
@ -57,11 +59,19 @@ const updateTimezoneIfNeeded = async () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
flex: {flex: 1},
|
||||||
|
});
|
||||||
|
|
||||||
export function HomeScreen(props: HomeProps) {
|
export function HomeScreen(props: HomeProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const appState = useAppState();
|
const appState = useAppState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initializeSecurityManager();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleFindChannels = useCallback(() => {
|
const handleFindChannels = useCallback(() => {
|
||||||
if (!NavigationStore.getScreensInStack().includes(Screens.FIND_CHANNELS)) {
|
if (!NavigationStore.getScreensInStack().includes(Screens.FIND_CHANNELS)) {
|
||||||
findChannels(
|
findChannels(
|
||||||
|
|
@ -136,7 +146,10 @@ export function HomeScreen(props: HomeProps) {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<View
|
||||||
|
style={styles.flex}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(Screens.HOME, true)}
|
||||||
|
>
|
||||||
<NavigationContainer
|
<NavigationContainer
|
||||||
theme={{
|
theme={{
|
||||||
...DefaultTheme,
|
...DefaultTheme,
|
||||||
|
|
@ -190,7 +203,7 @@ export function HomeScreen(props: HomeProps) {
|
||||||
</Tab.Navigator>
|
</Tab.Navigator>
|
||||||
</NavigationContainer>
|
</NavigationContainer>
|
||||||
<ServerVersion/>
|
<ServerVersion/>
|
||||||
</>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {openNotification} from '@actions/remote/notifications';
|
||||||
import {Navigation as NavigationTypes} from '@constants';
|
import {Navigation as NavigationTypes} from '@constants';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissOverlay} from '@screens/navigation';
|
import {dismissOverlay} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
import {changeOpacity} from '@utils/theme';
|
import {changeOpacity} from '@utils/theme';
|
||||||
|
|
@ -157,6 +158,7 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[styles.container, isTablet ? styles.tablet : undefined, animatedStyle]}
|
style={[styles.container, isTablet ? styles.tablet : undefined, animatedStyle]}
|
||||||
testID='in_app_notification.screen'
|
testID='in_app_notification.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {debounce} from '@helpers/api/general';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {
|
import {
|
||||||
buildNavigationButton,
|
buildNavigationButton,
|
||||||
popTopScreen, setButtons,
|
popTopScreen, setButtons,
|
||||||
|
|
@ -587,7 +588,10 @@ function IntegrationSelector(
|
||||||
const selectedOptionsComponent = renderSelectedOptions();
|
const selectedOptionsComponent = renderSelectedOptions();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaView style={style.container}>
|
<SafeAreaView
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
style={style.container}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
testID='integration_selector.screen'
|
testID='integration_selector.screen'
|
||||||
style={style.searchBar}
|
style={style.searchBar}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import ErrorText from '@components/error_text';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
@ -234,6 +235,7 @@ function InteractiveDialog({
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
testID='interactive_dialog.screen'
|
testID='interactive_dialog.screen'
|
||||||
style={style.container}
|
style={style.container}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<KeyboardAwareScrollView
|
<KeyboardAwareScrollView
|
||||||
ref={scrollView}
|
ref={scrollView}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {useKeyboardOverlap} from '@hooks/device';
|
import {useKeyboardOverlap} from '@hooks/device';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal, setButtons} from '@screens/navigation';
|
import {dismissModal, setButtons} from '@screens/navigation';
|
||||||
import {isEmail} from '@utils/helpers';
|
import {isEmail} from '@utils/helpers';
|
||||||
import {mergeNavigationOptions} from '@utils/navigation';
|
import {mergeNavigationOptions} from '@utils/navigation';
|
||||||
|
|
@ -416,6 +417,7 @@ export default function Invite({
|
||||||
onLayout={onLayoutWrapper}
|
onLayout={onLayoutWrapper}
|
||||||
ref={mainView}
|
ref={mainView}
|
||||||
testID='invite.screen'
|
testID='invite.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
{renderContent()}
|
{renderContent()}
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {logDebug} from '@utils/log';
|
import {logDebug} from '@utils/log';
|
||||||
import {alertTeamAddError} from '@utils/navigation';
|
import {alertTeamAddError} from '@utils/navigation';
|
||||||
|
|
@ -154,7 +155,10 @@ export default function JoinTeam({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
style={styles.container}
|
||||||
|
>
|
||||||
{body}
|
{body}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {SafeAreaView, type Edge} from 'react-native-safe-area-context';
|
||||||
import MathView from '@components/math_view';
|
import MathView from '@components/math_view';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import {splitLatexCodeInLines} from '@utils/markdown/latex';
|
import {splitLatexCodeInLines} from '@utils/markdown/latex';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
@ -81,6 +82,7 @@ const Latex = ({componentId, content}: Props) => {
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
edges={edges}
|
edges={edges}
|
||||||
style={style.scrollContainer}
|
style={style.scrollContainer}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={style.scrollContainer}
|
style={style.scrollContainer}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {useIsTablet} from '@hooks/device';
|
||||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {dismissModal, goToScreen, loginAnimationOptions, popTopScreen} from '@screens/navigation';
|
import {dismissModal, goToScreen, loginAnimationOptions, popTopScreen} from '@screens/navigation';
|
||||||
import {preventDoubleTap} from '@utils/tap';
|
import {preventDoubleTap} from '@utils/tap';
|
||||||
|
|
@ -213,6 +214,7 @@ const LoginOptions = ({
|
||||||
<View
|
<View
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID='login.screen'
|
testID='login.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
>
|
>
|
||||||
<Background theme={theme}/>
|
<Background theme={theme}/>
|
||||||
<AnimatedSafeArea style={[styles.container, transform]}>
|
<AnimatedSafeArea style={[styles.container, transform]}>
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {openAsBottomSheet, popTopScreen, setButtons} from '@screens/navigation';
|
import {openAsBottomSheet, popTopScreen, setButtons} from '@screens/navigation';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
|
import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
|
||||||
|
|
@ -291,6 +292,7 @@ export default function ManageChannelMembers({
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
testID='manage_members.screen'
|
testID='manage_members.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<View style={styles.searchBar}>
|
<View style={styles.searchBar}>
|
||||||
<Search
|
<Search
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import Loading from '@components/loading';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useAvoidKeyboard} from '@hooks/device';
|
import {useAvoidKeyboard} from '@hooks/device';
|
||||||
import {t} from '@i18n';
|
import {t} from '@i18n';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
|
|
@ -168,7 +169,10 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
|
||||||
useAndroidHardwareBackHandler(componentId, close);
|
useAndroidHardwareBackHandler(componentId, close);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.flex}>
|
<View
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
|
style={styles.flex}
|
||||||
|
>
|
||||||
<Background theme={theme}/>
|
<Background theme={theme}/>
|
||||||
<AnimatedSafeArea
|
<AnimatedSafeArea
|
||||||
testID='mfa.screen'
|
testID='mfa.screen'
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import Animated, {ReduceMotion, useAnimatedStyle, useDerivedValue, useSharedValu
|
||||||
|
|
||||||
import {storeOnboardingViewedValue} from '@actions/app/global';
|
import {storeOnboardingViewedValue} from '@actions/app/global';
|
||||||
import {Screens} from '@constants';
|
import {Screens} from '@constants';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {goToScreen, loginAnimationOptions} from '@screens/navigation';
|
import {goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||||
|
|
||||||
|
|
@ -27,8 +28,10 @@ import SlideItem from './slide';
|
||||||
import useSlidesData from './slides_data';
|
import useSlidesData from './slides_data';
|
||||||
|
|
||||||
import type {LaunchProps} from '@typings/launch';
|
import type {LaunchProps} from '@typings/launch';
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
interface OnboardingProps extends LaunchProps {
|
interface OnboardingProps extends LaunchProps {
|
||||||
|
componentId: AvailableScreens;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -49,6 +52,7 @@ const styles = StyleSheet.create({
|
||||||
const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
|
const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
|
||||||
|
|
||||||
const Onboarding = ({
|
const Onboarding = ({
|
||||||
|
componentId,
|
||||||
theme,
|
theme,
|
||||||
...props
|
...props
|
||||||
}: OnboardingProps) => {
|
}: OnboardingProps) => {
|
||||||
|
|
@ -133,6 +137,7 @@ const Onboarding = ({
|
||||||
<View
|
<View
|
||||||
style={styles.onBoardingContainer}
|
style={styles.onBoardingContainer}
|
||||||
testID='onboarding.screen'
|
testID='onboarding.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
>
|
>
|
||||||
<Background theme={theme}/>
|
<Background theme={theme}/>
|
||||||
<AnimatedSafeArea
|
<AnimatedSafeArea
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,20 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {type ReactNode} from 'react';
|
import React, {type ReactNode} from 'react';
|
||||||
|
import {View} from 'react-native';
|
||||||
|
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
componentId: AvailableScreens;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Overlay = ({children}: Props) => {
|
const Overlay = ({children, componentId}: Props) => {
|
||||||
return children;
|
return (<View nativeID={SecurityManager.getShieldScreenId(componentId)}>{children}</View>);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Overlay;
|
export default Overlay;
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ import {useTheme} from '@context/theme';
|
||||||
import DatabaseManager from '@database/manager';
|
import DatabaseManager from '@database/manager';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {getChannelById, getMyChannel} from '@queries/servers/channel';
|
import {getChannelById, getMyChannel} from '@queries/servers/channel';
|
||||||
import {dismissModal} from '@screens/navigation';
|
import {dismissModal} from '@screens/navigation';
|
||||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||||
|
|
@ -355,6 +356,7 @@ function Permalink({
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={containerStyle}
|
style={containerStyle}
|
||||||
testID='permalink.screen'
|
testID='permalink.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(Screens.PERMALINK)}
|
||||||
edges={edges}
|
edges={edges}
|
||||||
>
|
>
|
||||||
<Animated.View style={style.wrapper}>
|
<Animated.View style={style.wrapper}>
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {ExtraKeyboardProvider} from '@context/extra_keyboard';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||||
|
|
||||||
|
|
@ -152,6 +153,7 @@ function SavedMessages({
|
||||||
edges={edges}
|
edges={edges}
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID='pinned_messages.screen'
|
testID='pinned_messages.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<ExtraKeyboardProvider>
|
<ExtraKeyboardProvider>
|
||||||
<FlatList
|
<FlatList
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useBackNavigation from '@hooks/navigate_back';
|
import useBackNavigation from '@hooks/navigate_back';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissOverlay, showShareFeedbackOverlay} from '@screens/navigation';
|
import {dismissOverlay, showShareFeedbackOverlay} from '@screens/navigation';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
@ -168,7 +169,10 @@ const ReviewApp = ({
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
style={styles.root}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
testID='rate_app.screen'
|
testID='rate_app.screen'
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {addCurrentUserToTeam, fetchTeamsForComponent, handleTeamChange} from '@a
|
||||||
import Loading from '@components/loading';
|
import Loading from '@components/loading';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {logDebug} from '@utils/log';
|
import {logDebug} from '@utils/log';
|
||||||
import {alertTeamAddError} from '@utils/navigation';
|
import {alertTeamAddError} from '@utils/navigation';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
|
|
@ -21,6 +22,8 @@ import Header from './header';
|
||||||
import NoTeams from './no_teams';
|
import NoTeams from './no_teams';
|
||||||
import TeamList from './team_list';
|
import TeamList from './team_list';
|
||||||
|
|
||||||
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
||||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
|
@ -34,6 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
componentId: AvailableScreens;
|
||||||
nTeams: number;
|
nTeams: number;
|
||||||
firstTeamId?: string;
|
firstTeamId?: string;
|
||||||
}
|
}
|
||||||
|
|
@ -42,6 +46,7 @@ const safeAreaEdges = ['left' as const, 'right' as const];
|
||||||
const safeAreaStyle = {flex: 1};
|
const safeAreaStyle = {flex: 1};
|
||||||
|
|
||||||
const SelectTeam = ({
|
const SelectTeam = ({
|
||||||
|
componentId,
|
||||||
nTeams,
|
nTeams,
|
||||||
firstTeamId,
|
firstTeamId,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
|
@ -140,6 +145,7 @@ const SelectTeam = ({
|
||||||
mode='margin'
|
mode='margin'
|
||||||
edges={safeAreaEdges}
|
edges={safeAreaEdges}
|
||||||
style={safeAreaStyle}
|
style={safeAreaStyle}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<Animated.View style={top}/>
|
<Animated.View style={top}/>
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {t} from '@i18n';
|
||||||
import {getServerCredentials} from '@init/credentials';
|
import {getServerCredentials} from '@init/credentials';
|
||||||
import PushNotifications from '@init/push_notifications';
|
import PushNotifications from '@init/push_notifications';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {getServerByDisplayName, getServerByIdentifier} from '@queries/app/servers';
|
import {getServerByDisplayName, getServerByIdentifier} from '@queries/app/servers';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {dismissModal, goToScreen, loginAnimationOptions, popTopScreen} from '@screens/navigation';
|
import {dismissModal, goToScreen, loginAnimationOptions, popTopScreen} from '@screens/navigation';
|
||||||
|
|
@ -340,6 +341,22 @@ const Server = ({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data.config.MobileJailbreakProtection === 'true') {
|
||||||
|
const isJailbroken = await SecurityManager.isDeviceJailbroken(ping.url, data.config.SiteName);
|
||||||
|
if (isJailbroken) {
|
||||||
|
setConnecting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.config.MobileEnableBiometrics === 'true') {
|
||||||
|
const biometricsResult = await SecurityManager.authenticateWithBiometrics(ping.url, data.config.SiteName);
|
||||||
|
if (!biometricsResult) {
|
||||||
|
setConnecting(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const server = await getServerByIdentifier(data.config.DiagnosticId);
|
const server = await getServerByIdentifier(data.config.DiagnosticId);
|
||||||
const credentials = await getServerCredentials(ping.url);
|
const credentials = await getServerCredentials(ping.url);
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
|
|
@ -367,6 +384,7 @@ const Server = ({
|
||||||
<View
|
<View
|
||||||
style={styles.flex}
|
style={styles.flex}
|
||||||
testID='server.screen'
|
testID='server.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
>
|
>
|
||||||
<Background theme={theme}/>
|
<Background theme={theme}/>
|
||||||
<AnimatedSafeArea
|
<AnimatedSafeArea
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useBackNavigation from '@hooks/navigate_back';
|
import useBackNavigation from '@hooks/navigate_back';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissOverlay} from '@screens/navigation';
|
import {dismissOverlay} from '@screens/navigation';
|
||||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
@ -134,7 +135,10 @@ const ShareFeedback = ({
|
||||||
}), []);
|
}), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
style={styles.root}
|
||||||
|
>
|
||||||
<View
|
<View
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
testID='rate_app.screen'
|
testID='rate_app.screen'
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import {MESSAGE_TYPE, SNACK_BAR_CONFIG} from '@constants/snack_bar';
|
||||||
import {TABLET_SIDEBAR_WIDTH} from '@constants/view';
|
import {TABLET_SIDEBAR_WIDTH} from '@constants/view';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import {useIsTablet} from '@hooks/device';
|
import {useIsTablet} from '@hooks/device';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissOverlay} from '@screens/navigation';
|
import {dismissOverlay} from '@screens/navigation';
|
||||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||||
import {typography} from '@utils/typography';
|
import {typography} from '@utils/typography';
|
||||||
|
|
@ -261,6 +262,7 @@ const SnackBar = ({
|
||||||
<GestureDetector gesture={gesture}>
|
<GestureDetector gesture={gesture}>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={animatedMotion}
|
style={animatedMotion}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={FadeIn.duration(300)}
|
entering={FadeIn.duration(300)}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {Screens, Sso} from '@constants';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||||
import NetworkManager from '@managers/network_manager';
|
import NetworkManager from '@managers/network_manager';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import Background from '@screens/background';
|
import Background from '@screens/background';
|
||||||
import {dismissModal, popTopScreen, resetToHome} from '@screens/navigation';
|
import {dismissModal, popTopScreen, resetToHome} from '@screens/navigation';
|
||||||
import {getFullErrorMessage, isErrorWithUrl} from '@utils/errors';
|
import {getFullErrorMessage, isErrorWithUrl} from '@utils/errors';
|
||||||
|
|
@ -172,7 +173,10 @@ const SSO = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.flex}>
|
<View
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||||
|
style={styles.flex}
|
||||||
|
>
|
||||||
<Background theme={theme}/>
|
<Background theme={theme}/>
|
||||||
<AnimatedSafeArea style={[styles.flex, transform]}>
|
<AnimatedSafeArea style={[styles.flex, transform]}>
|
||||||
{authentication}
|
{authentication}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import React, {useCallback} from 'react';
|
import React, {useCallback} from 'react';
|
||||||
import {Platform, ScrollView, StyleSheet} from 'react-native';
|
import {Platform, ScrollView, StyleSheet, View} from 'react-native';
|
||||||
import {SafeAreaView} from 'react-native-safe-area-context';
|
import {SafeAreaView} from 'react-native-safe-area-context';
|
||||||
|
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen} from '@screens/navigation';
|
import {popTopScreen} from '@screens/navigation';
|
||||||
|
|
||||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||||
|
|
@ -48,15 +49,20 @@ const Table = ({componentId, renderAsFlex, renderRows, width}: Props) => {
|
||||||
|
|
||||||
if (Platform.OS === 'android') {
|
if (Platform.OS === 'android') {
|
||||||
return (
|
return (
|
||||||
<ScrollView testID='table.screen'>
|
<View
|
||||||
<ScrollView
|
style={styles.container}
|
||||||
contentContainerStyle={viewStyle}
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
horizontal={true}
|
>
|
||||||
testID='table.scroll_view'
|
<ScrollView testID='table.screen'>
|
||||||
>
|
<ScrollView
|
||||||
{content}
|
contentContainerStyle={viewStyle}
|
||||||
|
horizontal={true}
|
||||||
|
testID='table.scroll_view'
|
||||||
|
>
|
||||||
|
{content}
|
||||||
|
</ScrollView>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</ScrollView>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +70,7 @@ const Table = ({componentId, renderAsFlex, renderRows, width}: Props) => {
|
||||||
<SafeAreaView
|
<SafeAreaView
|
||||||
style={styles.container}
|
style={styles.container}
|
||||||
testID='table.screen'
|
testID='table.screen'
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={styles.fullHeight}
|
style={styles.fullHeight}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import {Screens} from '@constants/index';
|
||||||
import {useServerUrl} from '@context/server';
|
import {useServerUrl} from '@context/server';
|
||||||
import {useTheme} from '@context/theme';
|
import {useTheme} from '@context/theme';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {dismissOverlay} from '@screens/navigation';
|
import {dismissOverlay} from '@screens/navigation';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||||
|
|
@ -291,7 +292,10 @@ const TermsOfService = ({
|
||||||
}, [styles, insets]);
|
}, [styles, insets]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View
|
||||||
|
style={styles.root}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
|
>
|
||||||
<View style={containerStyle}>
|
<View style={containerStyle}>
|
||||||
<View style={styles.wrapper}>
|
<View style={styles.wrapper}>
|
||||||
<Text style={styles.title}>{intl.formatMessage({id: 'terms_of_service.title', defaultMessage: 'Terms of Service'})}</Text>
|
<Text style={styles.title}>{intl.formatMessage({id: 'terms_of_service.title', defaultMessage: 'Terms of Service'})}</Text>
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {Screens} from '@constants';
|
||||||
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
|
import {ExtraKeyboardProvider} from '@context/extra_keyboard';
|
||||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||||
import useDidUpdate from '@hooks/did_update';
|
import useDidUpdate from '@hooks/did_update';
|
||||||
|
import SecurityManager from '@managers/security_manager';
|
||||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||||
import EphemeralStore from '@store/ephemeral_store';
|
import EphemeralStore from '@store/ephemeral_store';
|
||||||
import NavigationStore from '@store/navigation_store';
|
import NavigationStore from '@store/navigation_store';
|
||||||
|
|
@ -114,6 +115,7 @@ const Thread = ({
|
||||||
edges={edges}
|
edges={edges}
|
||||||
testID='thread.screen'
|
testID='thread.screen'
|
||||||
onLayout={onLayout}
|
onLayout={onLayout}
|
||||||
|
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||||
>
|
>
|
||||||
<RoundedHeaderContext/>
|
<RoundedHeaderContext/>
|
||||||
{Boolean(rootPost) &&
|
{Boolean(rootPost) &&
|
||||||
|
|
|
||||||
|
|
@ -702,14 +702,20 @@
|
||||||
"mobile.manage_members.remove_member": "Remove from Channel",
|
"mobile.manage_members.remove_member": "Remove from Channel",
|
||||||
"mobile.manage_members.section_title_admins": "CHANNEL ADMINS",
|
"mobile.manage_members.section_title_admins": "CHANNEL ADMINS",
|
||||||
"mobile.manage_members.section_title_members": "MEMBERS",
|
"mobile.manage_members.section_title_members": "MEMBERS",
|
||||||
|
"mobile.managed.biometric_failed": "Biometric or Passcode authentication failed.",
|
||||||
"mobile.managed.blocked_by": "Blocked by {vendor}",
|
"mobile.managed.blocked_by": "Blocked by {vendor}",
|
||||||
"mobile.managed.exit": "Exit",
|
"mobile.managed.exit": "Exit",
|
||||||
"mobile.managed.jailbreak": "Jailbroken or rooted devices are not trusted by {vendor}.\n\nThe app will now close.",
|
"mobile.managed.jailbreak": "Jailbroken or rooted devices are not trusted by {vendor}.",
|
||||||
|
"mobile.managed.jailbreak.emm": "Jailbroken or rooted devices are not trusted by {vendor}.\n\nThe app will now close.",
|
||||||
|
"mobile.managed.logout": "Logout",
|
||||||
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
|
"mobile.managed.not_secured.android": "This device must be secured with a screen lock to use Mattermost.",
|
||||||
"mobile.managed.not_secured.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
|
"mobile.managed.not_secured.android.vendor": "This device must be secured with a screen lock to use {vendor}.",
|
||||||
"mobile.managed.not_secured.ios.touchId": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Touch ID & Passcode.",
|
"mobile.managed.not_secured.ios": "This device must be secured with biometrics or passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
|
||||||
|
"mobile.managed.not_secured.ios.vendor": "This device must be secured with biometrics or passcode to use {vendor}.\n\nGo to Settings > Face ID & Passcode.",
|
||||||
|
"mobile.managed.OK": "OK",
|
||||||
"mobile.managed.secured_by": "Secured by {vendor}",
|
"mobile.managed.secured_by": "Secured by {vendor}",
|
||||||
"mobile.managed.settings": "Go to settings",
|
"mobile.managed.settings": "Go to settings",
|
||||||
|
"mobile.managed.switch_server": "Switch server",
|
||||||
"mobile.markdown.code.copy_code": "Copy Code",
|
"mobile.markdown.code.copy_code": "Copy Code",
|
||||||
"mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}",
|
"mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}",
|
||||||
"mobile.markdown.copy_header": "Copy header text",
|
"mobile.markdown.copy_header": "Copy header text",
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,14 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Gekidou
|
import Gekidou
|
||||||
|
import react_native_emm
|
||||||
|
|
||||||
@objc class GekidouWrapper: NSObject {
|
@objc class GekidouWrapper: NSObject {
|
||||||
@objc public static let `default` = GekidouWrapper()
|
@objc public static let `default` = GekidouWrapper()
|
||||||
|
|
||||||
|
override init() {
|
||||||
|
ScreenCaptureManager.startTrackingScreens()
|
||||||
|
}
|
||||||
|
|
||||||
@objc func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
@objc func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
||||||
PushNotification.default.postNotificationReceipt(userInfo)
|
PushNotification.default.postNotificationReceipt(userInfo)
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ static SendReplyCompletionHandlerIMP originalSendReplyCompletionHandlerImplement
|
||||||
|
|
||||||
if (serverUrl == nil) {
|
if (serverUrl == nil) {
|
||||||
[self handleReplyFailure:@"" completionHandler:notificationCompletionHandler];
|
[self handleReplyFailure:@"" completionHandler:notificationCompletionHandler];
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSString *sessionToken = [[Keychain default] getTokenObjcFor:serverUrl];
|
NSString *sessionToken = [[Keychain default] getTokenObjcFor:serverUrl];
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ static URLSessionTaskDidReceiveChallengeIMP originalURLSessionTaskDidReceiveChal
|
||||||
delegateQueue:session.delegateQueue];
|
delegateQueue:session.delegateQueue];
|
||||||
NSURLRequest *authorizedRequest = [BearerAuthenticationAdapter addAuthorizationBearerTokenTo:request withSessionBaseUrlString:sessionBaseUrl.absoluteString];
|
NSURLRequest *authorizedRequest = [BearerAuthenticationAdapter addAuthorizationBearerTokenTo:request withSessionBaseUrlString:sessionBaseUrl.absoluteString];
|
||||||
|
|
||||||
return originalInitWithRequestInSessionOptionsContextImplementation(self, @selector(initWithRequest:inSession:options:context:), authorizedRequest, session, &options, context);
|
return originalInitWithRequestInSessionOptionsContextImplementation(self, @selector(initWithRequest:inSession:options:context:), authorizedRequest, newSession, &options, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
return originalInitWithRequestInSessionOptionsContextImplementation(self, @selector(initWithRequest:inSession:options:context:), request, session, &options, context);
|
return originalInitWithRequestInSessionOptionsContextImplementation(self, @selector(initWithRequest:inSession:options:context:), request, session, &options, context);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ PODS:
|
||||||
- DoubleConversion (1.1.6)
|
- DoubleConversion (1.1.6)
|
||||||
- EXApplication (6.0.1):
|
- EXApplication (6.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXConstants (17.0.5):
|
- EXConstants (17.0.3):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- Expo (52.0.19):
|
- Expo (52.0.19):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
|
@ -15,7 +15,7 @@ PODS:
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoDevice (7.0.1):
|
- ExpoDevice (7.0.1):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoFileSystem (18.0.7):
|
- ExpoFileSystem (18.0.6):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ExpoImage (2.0.4):
|
- ExpoImage (2.0.4):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
|
@ -1410,7 +1410,7 @@ PODS:
|
||||||
- ReactCommon/turbomodule/bridging
|
- ReactCommon/turbomodule/bridging
|
||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- Yoga
|
- Yoga
|
||||||
- react-native-emm (1.5.1):
|
- react-native-emm (1.6.0):
|
||||||
- DoubleConversion
|
- DoubleConversion
|
||||||
- glog
|
- glog
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
|
|
@ -2496,11 +2496,11 @@ SPEC CHECKSUMS:
|
||||||
CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0
|
CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0
|
||||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||||
EXApplication: 88ebf1a95f85faa5d2df160016d61f2d060e9438
|
EXApplication: 88ebf1a95f85faa5d2df160016d61f2d060e9438
|
||||||
EXConstants: e25c3f3ef5d1cf7a6ef911ebf7a3ab94d8d06d82
|
EXConstants: 277129d9a42ba2cf1fad375e7eaa9939005c60be
|
||||||
Expo: 848501c300b37b3fa7330c58a5353e07e03d5c3d
|
Expo: 848501c300b37b3fa7330c58a5353e07e03d5c3d
|
||||||
ExpoCrypto: 483fc758365923f89ddaab4327c21cae9534841d
|
ExpoCrypto: 483fc758365923f89ddaab4327c21cae9534841d
|
||||||
ExpoDevice: 449822f2c45660c1ce83283dd51c67fe5404996c
|
ExpoDevice: 449822f2c45660c1ce83283dd51c67fe5404996c
|
||||||
ExpoFileSystem: 818e82dbb71175414d1ca310e926c48ff0d07348
|
ExpoFileSystem: a38e1bb58d77f41717293a7b73ebd4014d8cb8dd
|
||||||
ExpoImage: 18ec1a3e5cd96ee6162d988be3085e18113e143f
|
ExpoImage: 18ec1a3e5cd96ee6162d988be3085e18113e143f
|
||||||
ExpoLinearGradient: 18148bd38f98fa0229c1f9df23393b32ab6d2d32
|
ExpoLinearGradient: 18148bd38f98fa0229c1f9df23393b32ab6d2d32
|
||||||
ExpoModulesCore: c9cb309323aee3c048099bb40ad0226e3fcf0c56
|
ExpoModulesCore: c9cb309323aee3c048099bb40ad0226e3fcf0c56
|
||||||
|
|
@ -2550,7 +2550,7 @@ SPEC CHECKSUMS:
|
||||||
react-native-cameraroll: 5f180ef5e9b52b6c3c3a2645fa33a921d1e37a6c
|
react-native-cameraroll: 5f180ef5e9b52b6c3c3a2645fa33a921d1e37a6c
|
||||||
react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411
|
react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411
|
||||||
react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659
|
react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659
|
||||||
react-native-emm: f6003bebdf4fef4feef7c61f96a5c174f43c6b5f
|
react-native-emm: 710fa6ce569cf04084224fa61a038e6143ea7fe0
|
||||||
react-native-image-picker: 130fad649d07e4eec8faaed361d3bba570e1e5ff
|
react-native-image-picker: 130fad649d07e4eec8faaed361d3bba570e1e5ff
|
||||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||||
react-native-network-client: fdbd7f5f4c2818d6b90b4dcf9613d0a54ab41303
|
react-native-network-client: fdbd7f5f4c2818d6b90b4dcf9613d0a54ab41303
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-process-env
|
||||||
|
process.env.TZ = 'UTC';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
preset: 'jest-expo',
|
preset: 'jest-expo',
|
||||||
verbose: true,
|
verbose: true,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,10 @@ class MattermostHardwareKeyboardImpl(reactApplicationContext: ReactApplicationCo
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun sendEvent(action: String) {
|
private fun sendEvent(action: String) {
|
||||||
|
if (!this::context.isInitialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (context.hasActiveReactInstance()) {
|
if (context.hasActiveReactInstance()) {
|
||||||
val result: WritableMap = WritableNativeMap()
|
val result: WritableMap = WritableNativeMap()
|
||||||
result.putString("action", action)
|
result.putString("action", action)
|
||||||
|
|
|
||||||
13
package-lock.json
generated
13
package-lock.json
generated
|
|
@ -20,7 +20,7 @@
|
||||||
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
||||||
"@mattermost/compass-icons": "0.1.48",
|
"@mattermost/compass-icons": "0.1.48",
|
||||||
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
||||||
"@mattermost/react-native-emm": "1.5.1",
|
"@mattermost/react-native-emm": "1.6.0",
|
||||||
"@mattermost/react-native-network-client": "1.8.2",
|
"@mattermost/react-native-network-client": "1.8.2",
|
||||||
"@mattermost/react-native-paste-input": "0.8.1",
|
"@mattermost/react-native-paste-input": "0.8.1",
|
||||||
"@mattermost/react-native-turbo-log": "0.6.0",
|
"@mattermost/react-native-turbo-log": "0.6.0",
|
||||||
|
|
@ -183,6 +183,11 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "Apache 2.0"
|
"license": "Apache 2.0"
|
||||||
},
|
},
|
||||||
|
"libraries/@mattermost/rnshield": {
|
||||||
|
"version": "0.0.0",
|
||||||
|
"extraneous": true,
|
||||||
|
"license": "Apache 2.0"
|
||||||
|
},
|
||||||
"libraries/@mattermost/rnutils": {
|
"libraries/@mattermost/rnutils": {
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "Apache 2.0"
|
"license": "Apache 2.0"
|
||||||
|
|
@ -4633,9 +4638,9 @@
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/@mattermost/react-native-emm": {
|
"node_modules/@mattermost/react-native-emm": {
|
||||||
"version": "1.5.1",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.6.0.tgz",
|
||||||
"integrity": "sha512-gkuj+UyPWqfxdG62lcqDT5301tPL5dgRpFm63HHegVs45bJX4LmNDHgF7MJuravOJjKX3huMpJDG/UnzHncbkQ==",
|
"integrity": "sha512-e8Ka0Zd6LZsbVWIiGeASiydV0WTQqc+2LNl8t/Zm22i0M7zhVgf4QIciXE/XjVJk1tfxJ5fhRM4LCp8q87HgTw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "*",
|
"react": "*",
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@
|
||||||
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
||||||
"@mattermost/compass-icons": "0.1.48",
|
"@mattermost/compass-icons": "0.1.48",
|
||||||
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
"@mattermost/hardware-keyboard": "file:./libraries/@mattermost/hardware-keyboard",
|
||||||
"@mattermost/react-native-emm": "1.5.1",
|
"@mattermost/react-native-emm": "1.6.0",
|
||||||
"@mattermost/react-native-network-client": "1.8.2",
|
"@mattermost/react-native-network-client": "1.8.2",
|
||||||
"@mattermost/react-native-paste-input": "0.8.1",
|
"@mattermost/react-native-paste-input": "0.8.1",
|
||||||
"@mattermost/react-native-turbo-log": "0.6.0",
|
"@mattermost/react-native-turbo-log": "0.6.0",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,18 @@
|
||||||
|
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx
|
||||||
|
index c79181a..31fc208 100644
|
||||||
|
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx
|
||||||
|
+++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheet/BottomSheet.tsx
|
||||||
|
@@ -1892,7 +1892,7 @@ const BottomSheetComponent = forwardRef<BottomSheet, BottomSheetProps>(
|
||||||
|
detached={detached}
|
||||||
|
style={_providedContainerStyle}
|
||||||
|
>
|
||||||
|
- <Animated.View style={containerStyle}>
|
||||||
|
+ <Animated.View style={containerStyle} nativeID={'BottomSheetComponent'}>
|
||||||
|
<BottomSheetBackgroundContainer
|
||||||
|
key="BottomSheetBackgroundContainer"
|
||||||
|
animatedIndex={animatedIndex}
|
||||||
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx
|
diff --git a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx
|
||||||
index 72fe2cb..abac348 100644
|
index 0716ecd..081d723 100644
|
||||||
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx
|
--- a/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx
|
||||||
+++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx
|
+++ b/node_modules/@gorhom/bottom-sheet/src/components/bottomSheetContainer/BottomSheetContainer.tsx
|
||||||
@@ -30,10 +30,10 @@ function BottomSheetContainerComponent({
|
@@ -30,10 +30,10 @@ function BottomSheetContainerComponent({
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ jest.mock('@nozbe/watermelondb/utils/common/randomId/randomId', () => ({}));
|
||||||
jest.mock('@database/manager');
|
jest.mock('@database/manager');
|
||||||
jest.doMock('react-native', () => {
|
jest.doMock('react-native', () => {
|
||||||
const {
|
const {
|
||||||
|
AppState: RNAppState,
|
||||||
Platform,
|
Platform,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
requireNativeComponent,
|
requireNativeComponent,
|
||||||
|
|
@ -87,6 +88,13 @@ jest.doMock('react-native', () => {
|
||||||
alert: jest.fn(),
|
alert: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AppState = {
|
||||||
|
...RNAppState,
|
||||||
|
addEventListener: jest.fn(() => ({
|
||||||
|
remove: jest.fn(),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
const InteractionManager = {
|
const InteractionManager = {
|
||||||
...RNInteractionManager,
|
...RNInteractionManager,
|
||||||
runAfterInteractions: jest.fn((cb) => cb()),
|
runAfterInteractions: jest.fn((cb) => cb()),
|
||||||
|
|
@ -214,10 +222,12 @@ jest.doMock('react-native', () => {
|
||||||
minor: 64,
|
minor: 64,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
select: jest.fn((dict) => dict.ios || dict.default),
|
||||||
},
|
},
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
requireNativeComponent,
|
requireNativeComponent,
|
||||||
Alert,
|
Alert,
|
||||||
|
AppState,
|
||||||
InteractionManager,
|
InteractionManager,
|
||||||
NativeModules,
|
NativeModules,
|
||||||
Linking,
|
Linking,
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import assert from 'assert';
|
||||||
|
|
||||||
import {random} from 'lodash';
|
import {random} from 'lodash';
|
||||||
import nock from 'nock';
|
import nock from 'nock';
|
||||||
|
import {of as of$} from 'rxjs';
|
||||||
|
|
||||||
import Config from '@assets/config.json';
|
import Config from '@assets/config.json';
|
||||||
import {Client} from '@client/rest';
|
import {Client} from '@client/rest';
|
||||||
|
|
@ -18,6 +19,7 @@ import DatabaseManager from '@database/manager';
|
||||||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||||
|
|
||||||
import type {APIClientInterface} from '@mattermost/react-native-network-client';
|
import type {APIClientInterface} from '@mattermost/react-native-network-client';
|
||||||
|
import type {Model, Query} from '@nozbe/watermelondb';
|
||||||
|
|
||||||
const DEFAULT_LOCALE = 'en';
|
const DEFAULT_LOCALE = 'en';
|
||||||
|
|
||||||
|
|
@ -731,6 +733,13 @@ class TestHelperSingleton {
|
||||||
|
|
||||||
wait = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
|
wait = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
|
||||||
tick = () => new Promise((r) => setImmediate(r));
|
tick = () => new Promise((r) => setImmediate(r));
|
||||||
|
|
||||||
|
mockQuery = <T extends Model>(returnValue: T | T[]) => {
|
||||||
|
return {
|
||||||
|
fetch: async () => returnValue,
|
||||||
|
observe: of$(returnValue),
|
||||||
|
} as unknown as Query<T>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const TestHelper = new TestHelperSingleton();
|
const TestHelper = new TestHelperSingleton();
|
||||||
|
|
|
||||||
5
types/api/config.d.ts
vendored
5
types/api/config.d.ts
vendored
|
|
@ -149,6 +149,9 @@ interface ClientConfig {
|
||||||
MaxNotificationsPerChannel: string;
|
MaxNotificationsPerChannel: string;
|
||||||
MaxPostSize: string;
|
MaxPostSize: string;
|
||||||
MinimumHashtagLength: string;
|
MinimumHashtagLength: string;
|
||||||
|
MobileEnableBiometrics: string;
|
||||||
|
MobileJailbreakProtection: string;
|
||||||
|
MobilePreventScreenCapture: string;
|
||||||
MobileExternalBrowser: string;
|
MobileExternalBrowser: string;
|
||||||
OpenIdButtonColor: string;
|
OpenIdButtonColor: string;
|
||||||
OpenIdButtonText: string;
|
OpenIdButtonText: string;
|
||||||
|
|
@ -198,3 +201,5 @@ interface ClientConfig {
|
||||||
WebsocketSecurePort: string;
|
WebsocketSecurePort: string;
|
||||||
WebsocketURL: string;
|
WebsocketURL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SecurityClientConfig = Pick<ClientConfig, 'MobileEnableBiometrics' | 'MobileJailbreakProtection' | 'MobilePreventScreenCapture' | 'SiteName'>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue