This commit is contained in:
parent
9b799154a1
commit
de39e0245f
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 {getServerCredentials} from '@init/credentials';
|
||||
import PerformanceMetricsManager from '@managers/performance_metrics_manager';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
|
||||
type AfterLoginArgs = {
|
||||
|
|
@ -33,8 +34,10 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
|
|||
|
||||
const credentials = await getServerCredentials(serverUrl);
|
||||
if (credentials?.token) {
|
||||
SecurityManager.addServer(serverUrl, clData.config, true);
|
||||
WebsocketManager.createClient(serverUrl, credentials.token);
|
||||
await WebsocketManager.initializeClient(serverUrl, 'Login');
|
||||
SecurityManager.setActiveServer(serverUrl);
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
|
|||
|
|
@ -6,8 +6,11 @@ import {ScrollView} from 'react-native';
|
|||
import {type Edge, SafeAreaView} from 'react-native-safe-area-context';
|
||||
|
||||
import {useTheme} from '@context/theme';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const edges: Edge[] = ['left', 'right', 'bottom'];
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
|
||||
|
|
@ -36,6 +39,7 @@ const SettingContainer = ({children, testID}: SettingContainerProps) => {
|
|||
edges={edges}
|
||||
style={styles.container}
|
||||
testID={`${testID}.screen`}
|
||||
nativeID={SecurityManager.getShieldScreenId(`${testID}.screen` as AvailableScreens)}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.contentContainerStyle}
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const setServerCredentials = (serverUrl: string, token: string) => {
|
|||
};
|
||||
|
||||
export const removeServerCredentials = async (serverUrl: string) => {
|
||||
return KeyChain.resetInternetCredentials(serverUrl);
|
||||
return KeyChain.resetInternetCredentials({server: serverUrl});
|
||||
};
|
||||
|
||||
export const removeActiveServerCredentials = async () => {
|
||||
|
|
|
|||
|
|
@ -4,15 +4,55 @@
|
|||
import Emm from '@mattermost/react-native-emm';
|
||||
import deepEqual from 'deep-equal';
|
||||
import {isRootedExperimentalAsync} from 'expo-device';
|
||||
import {defineMessages} from 'react-intl';
|
||||
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 {isMainActivity} from '@utils/helpers';
|
||||
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
|
||||
|
||||
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 {
|
||||
backgroundSince = 0;
|
||||
enabled = false;
|
||||
|
|
@ -94,11 +134,11 @@ class ManagedAppSingleton {
|
|||
const locale = DEFAULT_LOCALE;
|
||||
const translations = getTranslations(locale);
|
||||
Alert.alert(
|
||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
||||
translations[t('mobile.managed.jailbreak')].
|
||||
translations[messages.blocked.id].replace('{vendor}', this.vendor),
|
||||
translations[messages.jailbreak.id].
|
||||
replace('{vendor}', this.vendor),
|
||||
[{
|
||||
text: translations[t('mobile.managed.exit')],
|
||||
text: translations[messages.exit.id],
|
||||
style: 'destructive',
|
||||
onPress: () => {
|
||||
Emm.exitApp();
|
||||
|
|
@ -123,7 +163,7 @@ class ManagedAppSingleton {
|
|||
if (authExpired) {
|
||||
try {
|
||||
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,
|
||||
supressEnterPassword: true,
|
||||
});
|
||||
|
|
@ -158,12 +198,12 @@ class ManagedAppSingleton {
|
|||
};
|
||||
|
||||
showNotSecuredAlert = (translations: Record<string, string>) => {
|
||||
return new Promise(async (resolve) => { /* eslint-disable-line no-async-promise-executor */
|
||||
return new Promise((resolve) => {
|
||||
const buttons: AlertButton[] = [];
|
||||
|
||||
if (Platform.OS === 'android') {
|
||||
buttons.push({
|
||||
text: translations[t('mobile.managed.settings')],
|
||||
text: translations[messages.androidSettings.id],
|
||||
onPress: () => {
|
||||
Emm.openSecuritySettings();
|
||||
},
|
||||
|
|
@ -171,26 +211,22 @@ class ManagedAppSingleton {
|
|||
}
|
||||
|
||||
buttons.push({
|
||||
text: translations[t('mobile.managed.exit')],
|
||||
text: translations[messages.exit.id],
|
||||
onPress: resolve,
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
let message;
|
||||
if (Platform.OS === 'ios') {
|
||||
const {face} = await Emm.deviceSecureWith();
|
||||
|
||||
if (face) {
|
||||
message = translations[t('mobile.managed.not_secured.ios')];
|
||||
} else {
|
||||
message = translations[t('mobile.managed.not_secured.ios.touchId')];
|
||||
}
|
||||
if (this.vendor) {
|
||||
const platform = Platform.select({ios: messages.notSecuredVendorIOS.id, default: messages.notSecuredVendorAndroid.id});
|
||||
message = translations[platform].replace('{vendor}', this.vendor);
|
||||
} 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(
|
||||
translations[t('mobile.managed.blocked_by')].replace('{vendor}', this.vendor),
|
||||
translations[messages.blocked.id].replace('{vendor}', this.vendor),
|
||||
message,
|
||||
buttons,
|
||||
{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 PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {deleteFileCache, deleteFileCacheByDir} from '@utils/file';
|
||||
import {isMainActivity} from '@utils/helpers';
|
||||
|
|
@ -37,6 +38,7 @@ jest.mock('@init/credentials');
|
|||
jest.mock('@init/launch');
|
||||
jest.mock('@init/push_notifications');
|
||||
jest.mock('@managers/network_manager');
|
||||
jest.mock('@managers/security_manager');
|
||||
jest.mock('@managers/websocket_manager');
|
||||
jest.mock('@queries/app/servers');
|
||||
jest.mock('@queries/servers/user');
|
||||
|
|
@ -107,6 +109,7 @@ describe('SessionManager', () => {
|
|||
expect(PushNotifications.removeServerNotifications).toHaveBeenCalledWith(mockServerUrl);
|
||||
expect(NetworkManager.invalidateClient).toHaveBeenCalledWith(mockServerUrl);
|
||||
expect(WebsocketManager.invalidateClient).toHaveBeenCalledWith(mockServerUrl);
|
||||
expect(SecurityManager.removeServer).toHaveBeenCalledWith(mockServerUrl);
|
||||
});
|
||||
|
||||
it('should handle session expiration', async () => {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {getAllServerCredentials, removeServerCredentials} from '@init/credential
|
|||
import {relaunchApp} from '@init/launch';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getAllServers, getServerDisplayName} from '@queries/app/servers';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
|
|
@ -114,6 +115,7 @@ export class SessionManagerSingleton {
|
|||
cancelSessionNotification(serverUrl);
|
||||
await removeServerCredentials(serverUrl);
|
||||
PushNotifications.removeServerNotifications(serverUrl);
|
||||
SecurityManager.removeServer(serverUrl);
|
||||
|
||||
NetworkManager.invalidateClient(serverUrl);
|
||||
WebsocketManager.invalidateClient(serverUrl);
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ import {useTheme} from '@context/theme';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {
|
||||
allOrientations,
|
||||
|
|
@ -736,7 +737,10 @@ const CallScreen = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.wrapper}>
|
||||
<SafeAreaView
|
||||
style={style.wrapper}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<StatusBar barStyle={'light-content'}/>
|
||||
<View style={style.container}>
|
||||
{!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} = {};
|
||||
list.forEach((v) => {
|
||||
config[v.id] = v.value;
|
||||
});
|
||||
return config as ClientConfig;
|
||||
return config as T;
|
||||
};
|
||||
|
||||
export const getConfig = async (database: Database) => {
|
||||
|
|
@ -150,6 +150,12 @@ export const getConfig = async (database: Database) => {
|
|||
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) => {
|
||||
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> => {
|
||||
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 useDidUpdate from '@hooks/did_update';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {filterEmptyOptions} from '@utils/apps';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
||||
|
|
@ -385,6 +386,7 @@ function AppsFormComponent({
|
|||
<SafeAreaView
|
||||
testID='interactive_dialog.screen'
|
||||
style={style.container}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<ScrollView
|
||||
ref={scrollView}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {useTheme} from '@context/theme';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {hapticFeedback} from '@utils/general';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -198,11 +199,14 @@ const BottomSheet = ({
|
|||
if (isTablet) {
|
||||
const FooterComponent = footerComponent;
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={styles.view}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View style={styles.separator}/>
|
||||
{renderContainerContent()}
|
||||
{FooterComponent && (<FooterComponent/>)}
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal, goToScreen, setButtons} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
|
@ -231,7 +232,10 @@ export default function BrowseChannels(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<SafeAreaView
|
||||
style={style.container}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{content}
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {useChannelSwitch} from '@hooks/channel_switch';
|
|||
import {useIsTablet} from '@hooks/device';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
import {useTeamSwitch} from '@hooks/team_switch';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
|
||||
|
|
@ -116,6 +117,7 @@ const Channel = ({
|
|||
edges={edges}
|
||||
testID='channel.screen'
|
||||
onLayout={onLayout}
|
||||
nativeID={componentId ? SecurityManager.getShieldScreenId(componentId) : undefined}
|
||||
>
|
||||
<ChannelHeader
|
||||
channelId={channelId}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
|||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {t} from '@i18n';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
|
|
@ -255,6 +256,7 @@ export default function ChannelAddMembers({
|
|||
onLayout={onLayout}
|
||||
ref={mainView}
|
||||
edges={['top', 'left', 'right']}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View style={style.searchBar}>
|
||||
<Search
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -269,6 +270,7 @@ const ChannelBookmarkAddOrEdit = ({
|
|||
edges={edges}
|
||||
style={styles.content}
|
||||
testID='channel_bookmark.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{type === 'link' &&
|
||||
<BookmarkLink
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {General} from '@constants';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {type FileFilter, FileFilters, filterFileExtensions} from '@utils/file';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
|
@ -135,48 +136,53 @@ function ChannelFiles({
|
|||
const fileChannels = useMemo(() => [channel], [channel]);
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
<View
|
||||
style={styles.flex}
|
||||
testID={`${TEST_ID}.screen`}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<Search
|
||||
testID={`${TEST_ID}.search_bar`}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onTextChange}
|
||||
onCancel={clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
testID={`${TEST_ID}.screen`}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<Search
|
||||
testID={`${TEST_ID}.search_bar`}
|
||||
placeholder={formatMessage({id: 'search_bar.search', defaultMessage: 'Search'})}
|
||||
cancelButtonTitle={formatMessage({id: 'mobile.post.cancel', defaultMessage: 'Cancel'})}
|
||||
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
|
||||
onChangeText={onTextChange}
|
||||
onCancel={clearSearch}
|
||||
autoCapitalize='none'
|
||||
keyboardAppearance={getKeyboardAppearanceFromTheme(theme)}
|
||||
value={term}
|
||||
/>
|
||||
</View>
|
||||
<Header
|
||||
onFilterChanged={handleFilterChange}
|
||||
selectedFilter={filter}
|
||||
/>
|
||||
</View>
|
||||
<Header
|
||||
onFilterChanged={handleFilterChange}
|
||||
selectedFilter={filter}
|
||||
/>
|
||||
{loading &&
|
||||
{loading &&
|
||||
<Loading
|
||||
color={theme.buttonBg}
|
||||
size='large'
|
||||
containerStyle={styles.loading}
|
||||
/>
|
||||
}
|
||||
{!loading &&
|
||||
<FileResults
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
fileChannels={fileChannels}
|
||||
fileInfos={fileInfos}
|
||||
paddingTop={styles.noPaddingTop}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
searchValue={term}
|
||||
isChannelFiles={true}
|
||||
isFilterEnabled={filter !== FileFilters.ALL}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
}
|
||||
{!loading &&
|
||||
<FileResults
|
||||
canDownloadFiles={canDownloadFiles}
|
||||
fileChannels={fileChannels}
|
||||
fileInfos={fileInfos}
|
||||
paddingTop={styles.noPaddingTop}
|
||||
publicLinkEnabled={publicLinkEnabled}
|
||||
searchValue={term}
|
||||
isChannelFiles={true}
|
||||
isFilterEnabled={filter !== FileFilters.ALL}
|
||||
/>
|
||||
}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
||||
|
|
@ -96,73 +97,78 @@ const ChannelInfo = ({
|
|||
const convertGMOptionAvailable = isConvertGMFeatureAvailable && type === General.GM_CHANNEL && !isGuestUser;
|
||||
|
||||
return (
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
<View
|
||||
style={styles.flex}
|
||||
testID='channel_info.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<ScrollView
|
||||
bounces={true}
|
||||
alwaysBounceVertical={false}
|
||||
contentContainerStyle={styles.content}
|
||||
testID='channel_info.scroll_view'
|
||||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
testID='channel_info.screen'
|
||||
>
|
||||
<Title
|
||||
channelId={channelId}
|
||||
type={type}
|
||||
/>
|
||||
{isBookmarksEnabled &&
|
||||
<ChannelBookmarks
|
||||
<ScrollView
|
||||
bounces={true}
|
||||
alwaysBounceVertical={false}
|
||||
contentContainerStyle={styles.content}
|
||||
testID='channel_info.scroll_view'
|
||||
>
|
||||
<Title
|
||||
channelId={channelId}
|
||||
canAddBookmarks={canAddBookmarks}
|
||||
showInInfo={true}
|
||||
type={type}
|
||||
/>
|
||||
}
|
||||
<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}/>
|
||||
</>
|
||||
}
|
||||
{canEnableDisableCalls &&
|
||||
<>
|
||||
<ChannelInfoEnableCalls
|
||||
{isBookmarksEnabled &&
|
||||
<ChannelBookmarks
|
||||
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}/>
|
||||
</>
|
||||
}
|
||||
<ChannelInfoAppBindings
|
||||
channelId={channelId}
|
||||
serverUrl={serverUrl}
|
||||
dismissChannelInfo={onPressed}
|
||||
/>
|
||||
<DestructiveOptions
|
||||
channelId={channelId}
|
||||
componentId={componentId}
|
||||
type={type}
|
||||
/>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
}
|
||||
{canEnableDisableCalls &&
|
||||
<>
|
||||
<ChannelInfoEnableCalls
|
||||
channelId={channelId}
|
||||
enabled={isCallsEnabledInChannel}
|
||||
/>
|
||||
<View style={styles.separator}/>
|
||||
</>
|
||||
}
|
||||
<ChannelInfoAppBindings
|
||||
channelId={channelId}
|
||||
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 useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {showSnackBar} from '@utils/snack_bar';
|
||||
|
||||
|
|
@ -64,6 +65,7 @@ const Code = ({code, componentId, language, textStyle}: Props) => {
|
|||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={styles.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<SyntaxHiglight
|
||||
code={code}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import AutocompleteSelector from '@components/autocomplete_selector';
|
|||
import {Preferences} from '@constants';
|
||||
import {CustomThemeProvider} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
|
||||
import ButtonComponentLibrary from './button.cl';
|
||||
|
|
@ -111,7 +112,10 @@ const ComponentLibrary = ({componentId}: Props) => {
|
|||
|
||||
const SelectedComponent = componentMap[selectedComponent];
|
||||
return (
|
||||
<ScrollView style={{margin: 10}}>
|
||||
<ScrollView
|
||||
style={{margin: 10}}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<AutocompleteSelector
|
||||
testID='selectedComponent'
|
||||
label='Component'
|
||||
|
|
|
|||
|
|
@ -3,19 +3,24 @@
|
|||
|
||||
import React, {useEffect, useRef, useState} from 'react';
|
||||
import {useIntl} from 'react-intl';
|
||||
import {View} from 'react-native';
|
||||
|
||||
import {fetchChannelMemberships, fetchGroupMessageMembersCommonTeams} from '@actions/remote/channel';
|
||||
import {PER_PAGE_DEFAULT} from '@client/rest/constants';
|
||||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
||||
import ConvertGMToChannelForm from './convert_gm_to_channel_form';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
type Props = {
|
||||
channelId: string;
|
||||
componentId: AvailableScreens;
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
|
|
@ -61,11 +66,13 @@ const getStyleFromTheme = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
flexDirection: 'column',
|
||||
gap: 24,
|
||||
},
|
||||
flex: {flex: 1},
|
||||
};
|
||||
});
|
||||
|
||||
const ConvertGMToChannel = ({
|
||||
channelId,
|
||||
componentId,
|
||||
currentUserId,
|
||||
}: Props) => {
|
||||
const theme = useTheme();
|
||||
|
|
@ -131,8 +138,9 @@ const ConvertGMToChannel = ({
|
|||
|
||||
const showLoader = !loadingAnimationTimeout || !commonTeamsFetched || !channelMembersFetched;
|
||||
|
||||
let component;
|
||||
if (showLoader) {
|
||||
return (
|
||||
component = (
|
||||
<Loading
|
||||
containerStyle={styles.loadingContainer}
|
||||
size='large'
|
||||
|
|
@ -141,14 +149,23 @@ const ConvertGMToChannel = ({
|
|||
footerTextStyles={styles.text}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
component = (
|
||||
<ConvertGMToChannelForm
|
||||
commonTeams={commonTeams}
|
||||
profiles={profiles}
|
||||
channelId={channelId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConvertGMToChannelForm
|
||||
commonTeams={commonTeams}
|
||||
profiles={profiles}
|
||||
channelId={channelId}
|
||||
/>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.flex}
|
||||
>
|
||||
{component}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@ import {StyleSheet, View} from 'react-native';
|
|||
import SearchBar from '@components/search';
|
||||
import TeamList from '@components/team_list';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {usePreventDoubleTap} from '@hooks/utils';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
padding: 12,
|
||||
|
|
@ -22,11 +25,12 @@ const styles = StyleSheet.create({
|
|||
});
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
teams: Team[];
|
||||
selectTeam: (teamId: string) => void;
|
||||
}
|
||||
|
||||
const TeamSelectorList = ({teams, selectTeam}: Props) => {
|
||||
const TeamSelectorList = ({componentId, teams, selectTeam}: Props) => {
|
||||
const theme = useTheme();
|
||||
const [filteredTeams, setFilteredTeam] = useState(teams);
|
||||
const color = useMemo(() => changeOpacity(theme.centerChannelColor, 0.72), [theme]);
|
||||
|
|
@ -39,13 +43,16 @@ const TeamSelectorList = ({teams, selectTeam}: Props) => {
|
|||
}
|
||||
}, 200), [teams]);
|
||||
|
||||
const handleOnPress = useCallback(preventDoubleTap((teamId: string) => {
|
||||
const handleOnPress = usePreventDoubleTap(useCallback((teamId: string) => {
|
||||
selectTeam(teamId);
|
||||
popTopScreen();
|
||||
}), []);
|
||||
}, []));
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.container}
|
||||
>
|
||||
<SearchBar
|
||||
autoCapitalize='none'
|
||||
autoFocus={true}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import {useTheme} from '@context/theme';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
import {alertErrorWithFallback} from '@utils/draft';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -298,6 +299,7 @@ export default function CreateDirectMessage({
|
|||
<SafeAreaView
|
||||
style={style.container}
|
||||
testID='create_direct_message.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
onLayout={onLayout}
|
||||
ref={mainView}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import React, {useCallback, useEffect, useMemo, useReducer, useState} from 'react';
|
||||
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 CompassIcon from '@components/compass_icon';
|
||||
|
|
@ -13,6 +13,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {buildNavigationButton, dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
||||
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);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const CreateOrEditChannel = ({
|
||||
componentId,
|
||||
channel,
|
||||
|
|
@ -233,21 +240,26 @@ const CreateOrEditChannel = ({
|
|||
useAndroidHardwareBackHandler(componentId, handleClose);
|
||||
|
||||
return (
|
||||
<ChannelInfoForm
|
||||
error={appState.error}
|
||||
saving={appState.saving}
|
||||
channelType={channel?.type}
|
||||
editing={editing}
|
||||
onTypeChange={setType}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
onDisplayNameChange={setDisplayName}
|
||||
header={header}
|
||||
headerOnly={headerOnly}
|
||||
onHeaderChange={setHeader}
|
||||
purpose={purpose}
|
||||
onPurposeChange={setPurpose}
|
||||
/>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.container}
|
||||
>
|
||||
<ChannelInfoForm
|
||||
error={appState.error}
|
||||
saving={appState.saving}
|
||||
channelType={channel?.type}
|
||||
editing={editing}
|
||||
onTypeChange={setType}
|
||||
type={type}
|
||||
displayName={displayName}
|
||||
onDisplayNameChange={setDisplayName}
|
||||
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 {useIsTablet} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal, goToScreen, openAsBottomSheet, showModal} from '@screens/navigation';
|
||||
import {getCurrentMomentForTimezone, getRoundedTime} from '@utils/helpers';
|
||||
import {logDebug} from '@utils/log';
|
||||
|
|
@ -75,6 +76,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
|
|||
borderTopColor: changeOpacity(theme.centerChannelColor, 0.1),
|
||||
borderTopWidth: 1,
|
||||
},
|
||||
flex: {flex: 1},
|
||||
};
|
||||
});
|
||||
|
||||
|
|
@ -345,7 +347,10 @@ const CustomStatus = ({
|
|||
}, [isBtnEnabled]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={style.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{isTablet &&
|
||||
<TabletTitle
|
||||
action={intl.formatMessage({id: 'mobile.custom_status.modal_confirm', defaultMessage: 'Done'})}
|
||||
|
|
@ -411,7 +416,7 @@ const CustomStatus = ({
|
|||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from 'react-native-navigation';
|
||||
|
||||
import {CustomStatusDurationEnum} from '@constants/custom_status';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {observeCurrentUser} from '@queries/servers/user';
|
||||
import {dismissModal, popTopScreen} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
|
|
@ -180,13 +181,14 @@ class ClearAfterModal extends NavigationComponent<Props, State> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const {currentUser, theme} = this.props;
|
||||
const {componentId, currentUser, theme} = this.props;
|
||||
const style = getStyleSheet(theme);
|
||||
const {duration, expiresAt, showExpiryTime} = this.state;
|
||||
return (
|
||||
<SafeAreaView
|
||||
style={style.container}
|
||||
testID='custom_status_clear_after.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<KeyboardAwareScrollView bounces={false}>
|
||||
<View style={style.scrollView}>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {useKeyboardOverlap} from '@hooks/device';
|
|||
import useDidUpdate from '@hooks/did_update';
|
||||
import {useInputPropagation} from '@hooks/input';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import PostError from '@screens/edit_post/post_error';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
|
@ -207,7 +208,10 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
|
||||
if (isUpdating) {
|
||||
return (
|
||||
<View style={styles.loader}>
|
||||
<View
|
||||
style={styles.loader}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<Loading color={theme.buttonBg}/>
|
||||
</View>
|
||||
);
|
||||
|
|
@ -219,6 +223,7 @@ const EditPost = ({componentId, maxPostSize, post, closeButtonId, hasFilesAttach
|
|||
testID='edit_post.screen'
|
||||
style={styles.container}
|
||||
onLayout={onLayout}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View
|
||||
style={styles.body}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
||||
|
|
@ -295,7 +296,10 @@ const EditProfile = ({
|
|||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={styles.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{isTablet &&
|
||||
<TabletTitle
|
||||
action={buttonText}
|
||||
|
|
@ -312,7 +316,7 @@ const EditProfile = ({
|
|||
>
|
||||
{content}
|
||||
</SafeAreaView>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {getServerByDisplayName} from '@queries/app/servers';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
|
|
@ -93,7 +94,10 @@ const EditServer = ({closeButtonId, componentId, server, theme}: ServerProps) =>
|
|||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<View
|
||||
style={styles.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<Background theme={theme}/>
|
||||
<SafeAreaView
|
||||
key={'server_content'}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {searchCustomEmojis} from '@actions/remote/custom_emoji';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {debounce} from '@helpers/api/general';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {getKeyboardAppearanceFromTheme} from '@utils/theme';
|
||||
|
||||
import EmojiFiltered from './filtered';
|
||||
|
|
@ -15,6 +16,7 @@ import PickerHeader from './header';
|
|||
import EmojiSections from './sections';
|
||||
|
||||
import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
export const SCROLLVIEW_NATIVE_ID = 'emojiSelector';
|
||||
|
||||
|
|
@ -82,6 +84,7 @@ const Picker = ({customEmojis, customEmojisEnabled, file, imageUrl, onEmojiPress
|
|||
<View
|
||||
style={styles.flex}
|
||||
testID={`${testID}.screen`}
|
||||
nativeID={SecurityManager.getShieldScreenId(testID as AvailableScreens)}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<PickerHeader
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {useTheme} from '@context/theme';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {changeOpacity, getKeyboardAppearanceFromTheme, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -87,6 +88,7 @@ const FindChannels = ({closeButtonId, componentId}: Props) => {
|
|||
<View
|
||||
style={styles.container}
|
||||
testID='find_channels.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<SearchBar
|
||||
autoCapitalize='none'
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import FormattedText from '@components/formatted_text';
|
|||
import {Screens} from '@constants';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useAvoidKeyboard} from '@hooks/device';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import Background from '@screens/background';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
import {isEmail} from '@utils/helpers';
|
||||
|
|
@ -141,6 +142,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
|
|||
<View
|
||||
style={styles.successContainer}
|
||||
testID={'password_send.link.sent'}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
>
|
||||
<Inbox/>
|
||||
<FormattedText
|
||||
|
|
@ -184,6 +186,7 @@ const ForgotPassword = ({componentId, serverUrl, theme}: Props) => {
|
|||
ref={keyboardAwareRef}
|
||||
scrollToOverflowEnabled={true}
|
||||
style={styles.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
>
|
||||
<View
|
||||
style={styles.centered}
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@
|
|||
|
||||
import RNUtils from '@mattermost/rnutils';
|
||||
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 {hasCaptions} from '@calls/utils';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet, useWindowDimensions} from '@hooks/device';
|
||||
import {useGalleryControls} from '@hooks/gallery';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissOverlay, setScreensOrientation} from '@screens/navigation';
|
||||
import {freezeOtherScreens} from '@utils/gallery';
|
||||
|
||||
|
|
@ -28,6 +29,10 @@ type Props = {
|
|||
items: GalleryItemType[];
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
flex: {flex: 1},
|
||||
});
|
||||
|
||||
const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialIndex, items}: Props) => {
|
||||
const dim = useWindowDimensions();
|
||||
const isTablet = useIsTablet();
|
||||
|
|
@ -84,30 +89,35 @@ const GalleryScreen = ({componentId, galleryIdentifier, hideActions, initialInde
|
|||
|
||||
return (
|
||||
<CaptionsEnabledContext.Provider value={captionsEnabled}>
|
||||
<Header
|
||||
index={localIndex}
|
||||
onClose={onClose}
|
||||
style={headerStyles}
|
||||
total={items.length}
|
||||
/>
|
||||
<Gallery
|
||||
galleryIdentifier={galleryIdentifier}
|
||||
initialIndex={initialIndex}
|
||||
items={items}
|
||||
onHide={close}
|
||||
onIndexChange={onIndexChange}
|
||||
onShouldHideControls={setControlsHidden}
|
||||
ref={galleryRef}
|
||||
targetDimensions={dimensions}
|
||||
/>
|
||||
<Footer
|
||||
hideActions={hideActions}
|
||||
item={items[localIndex]}
|
||||
style={footerStyles}
|
||||
hasCaptions={captionsAvailable[localIndex]}
|
||||
captionEnabled={captionsEnabled[localIndex]}
|
||||
onCaptionsPress={onCaptionsPress}
|
||||
/>
|
||||
<View
|
||||
style={styles.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<Header
|
||||
index={localIndex}
|
||||
onClose={onClose}
|
||||
style={headerStyles}
|
||||
total={items.length}
|
||||
/>
|
||||
<Gallery
|
||||
galleryIdentifier={galleryIdentifier}
|
||||
initialIndex={initialIndex}
|
||||
items={items}
|
||||
onHide={close}
|
||||
onIndexChange={onIndexChange}
|
||||
onShouldHideControls={setControlsHidden}
|
||||
ref={galleryRef}
|
||||
targetDimensions={dimensions}
|
||||
/>
|
||||
<Footer
|
||||
hideActions={hideActions}
|
||||
item={items[localIndex]}
|
||||
style={footerStyles}
|
||||
hasCaptions={captionsAvailable[localIndex]}
|
||||
captionEnabled={captionsEnabled[localIndex]}
|
||||
onCaptionsPress={onCaptionsPress}
|
||||
/>
|
||||
</View>
|
||||
</CaptionsEnabledContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {Screens} from '@constants';
|
|||
import {useIsTablet} from '@hooks/device';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
import {useTeamSwitch} from '@hooks/team_switch';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
|
||||
import {popTopScreen} from '../navigation';
|
||||
|
||||
|
|
@ -69,6 +70,7 @@ const GlobalDrafts = ({componentId}: Props) => {
|
|||
mode='margin'
|
||||
style={styles.flex}
|
||||
testID='global_drafts.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId || Screens.GLOBAL_DRAFTS)}
|
||||
>
|
||||
<NavigationHeader
|
||||
showBackButton={!isTablet}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
|||
import {useIsTablet} from '@hooks/device';
|
||||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
import {useTeamSwitch} from '@hooks/team_switch';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
|
||||
import ThreadsList from './threads_list';
|
||||
|
|
@ -84,6 +85,7 @@ const GlobalThreads = ({componentId, globalThreadsTab}: Props) => {
|
|||
mode='margin'
|
||||
style={styles.flex}
|
||||
testID='global_threads.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId || Screens.GLOBAL_THREADS)}
|
||||
>
|
||||
<NavigationHeader
|
||||
showBackButton={!isTablet}
|
||||
|
|
|
|||
|
|
@ -9,9 +9,8 @@ import Swipeable from 'react-native-gesture-handler/Swipeable';
|
|||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
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 {fetchConfigAndLicense} from '@actions/remote/systems';
|
||||
import CompassIcon from '@components/compass_icon';
|
||||
import Loading from '@components/loading';
|
||||
import ServerIcon from '@components/server_icon';
|
||||
|
|
@ -20,14 +19,10 @@ import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left';
|
|||
import {Events, Screens} from '@constants';
|
||||
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
|
||||
import {useTheme} from '@context/theme';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {subscribeServerUnreadAndMentions, type UnreadObserverArgs} from '@database/subscription/unreads';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import WebsocketManager from '@managers/websocket_manager';
|
||||
import {getServerByIdentifier} from '@queries/app/servers';
|
||||
import {dismissBottomSheet} from '@screens/navigation';
|
||||
import {canReceiveNotifications} from '@utils/push_proxy';
|
||||
import {alertServerAlreadyConnected, alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
|
||||
import {alertServerLogout, alertServerRemove, editServer} from '@utils/server';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
import {removeProtocol, stripTrailingSlashes} from '@utils/url';
|
||||
|
|
@ -253,29 +248,8 @@ const ServerItem = ({
|
|||
const handleLogin = useCallback(async () => {
|
||||
swipeable.current?.close();
|
||||
setSwitching(true);
|
||||
const result = await doPing(server.url, true);
|
||||
if (result.error) {
|
||||
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]);
|
||||
await switchToServerAndLogin(server.url, theme, intl, () => setSwitching(false));
|
||||
}, [server.url, intl, theme]);
|
||||
|
||||
const handleDismissTutorial = useCallback(() => {
|
||||
swipeable.current?.close();
|
||||
|
|
@ -309,14 +283,9 @@ const ServerItem = ({
|
|||
if (server.lastActiveAt) {
|
||||
setSwitching(true);
|
||||
await dismissBottomSheet();
|
||||
Navigation.updateProps(Screens.HOME, {extra: undefined});
|
||||
DatabaseManager.setActiveServerDatabase(server.url);
|
||||
WebsocketManager.initializeClient(server.url, 'Server Switch');
|
||||
return;
|
||||
}
|
||||
|
||||
handleLogin();
|
||||
}, [isActive, server.lastActiveAt, server.url, handleLogin]);
|
||||
await switchToServer(server.url, theme, intl, () => setSwitching(false));
|
||||
}, [isActive, server.lastActiveAt, server.url, theme, intl]);
|
||||
|
||||
const onSwipeableWillOpen = useCallback(() => {
|
||||
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 React, {useCallback, useEffect, useMemo} from 'react';
|
||||
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 {initializeSecurityManager} from '@actions/app/server';
|
||||
import {autoUpdateTimezone} from '@actions/remote/user';
|
||||
import ServerVersion from '@components/server_version';
|
||||
import {Events, Launch, Screens} from '@constants';
|
||||
import {useTheme} from '@context/theme';
|
||||
import {useAppState} from '@hooks/device';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {getAllServers} from '@queries/app/servers';
|
||||
import {findChannels, popToRoot} from '@screens/navigation';
|
||||
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) {
|
||||
const theme = useTheme();
|
||||
const intl = useIntl();
|
||||
const appState = useAppState();
|
||||
|
||||
useEffect(() => {
|
||||
initializeSecurityManager();
|
||||
}, []);
|
||||
|
||||
const handleFindChannels = useCallback(() => {
|
||||
if (!NavigationStore.getScreensInStack().includes(Screens.FIND_CHANNELS)) {
|
||||
findChannels(
|
||||
|
|
@ -136,7 +146,10 @@ export function HomeScreen(props: HomeProps) {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<View
|
||||
style={styles.flex}
|
||||
nativeID={SecurityManager.getShieldScreenId(Screens.HOME, true)}
|
||||
>
|
||||
<NavigationContainer
|
||||
theme={{
|
||||
...DefaultTheme,
|
||||
|
|
@ -190,7 +203,7 @@ export function HomeScreen(props: HomeProps) {
|
|||
</Tab.Navigator>
|
||||
</NavigationContainer>
|
||||
<ServerVersion/>
|
||||
</>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {openNotification} from '@actions/remote/notifications';
|
|||
import {Navigation as NavigationTypes} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissOverlay} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
import {changeOpacity} from '@utils/theme';
|
||||
|
|
@ -157,6 +158,7 @@ const InAppNotification = ({componentId, serverName, serverUrl, notification}: I
|
|||
<Animated.View
|
||||
style={[styles.container, isTablet ? styles.tablet : undefined, animatedStyle]}
|
||||
testID='in_app_notification.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View style={styles.flex}>
|
||||
<TouchableOpacity
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import {debounce} from '@helpers/api/general';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import {t} from '@i18n';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {
|
||||
buildNavigationButton,
|
||||
popTopScreen, setButtons,
|
||||
|
|
@ -587,7 +588,10 @@ function IntegrationSelector(
|
|||
const selectedOptionsComponent = renderSelectedOptions();
|
||||
|
||||
return (
|
||||
<SafeAreaView style={style.container}>
|
||||
<SafeAreaView
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={style.container}
|
||||
>
|
||||
<View
|
||||
testID='integration_selector.screen'
|
||||
style={style.searchBar}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import ErrorText from '@components/error_text';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {buildNavigationButton, dismissModal, setButtons} from '@screens/navigation';
|
||||
import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -234,6 +235,7 @@ function InteractiveDialog({
|
|||
<SafeAreaView
|
||||
testID='interactive_dialog.screen'
|
||||
style={style.container}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<KeyboardAwareScrollView
|
||||
ref={scrollView}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import {useKeyboardOverlap} from '@hooks/device';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal, setButtons} from '@screens/navigation';
|
||||
import {isEmail} from '@utils/helpers';
|
||||
import {mergeNavigationOptions} from '@utils/navigation';
|
||||
|
|
@ -416,6 +417,7 @@ export default function Invite({
|
|||
onLayout={onLayoutWrapper}
|
||||
ref={mainView}
|
||||
testID='invite.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
{renderContent()}
|
||||
</SafeAreaView>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {alertTeamAddError} from '@utils/navigation';
|
||||
|
|
@ -154,7 +155,10 @@ export default function JoinTeam({
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.container}
|
||||
>
|
||||
{body}
|
||||
</View>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import {SafeAreaView, type Edge} from 'react-native-safe-area-context';
|
|||
import MathView from '@components/math_view';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {splitLatexCodeInLines} from '@utils/markdown/latex';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -81,6 +82,7 @@ const Latex = ({componentId, content}: Props) => {
|
|||
<SafeAreaView
|
||||
edges={edges}
|
||||
style={style.scrollContainer}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<ScrollView
|
||||
style={style.scrollContainer}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {useIsTablet} from '@hooks/device';
|
|||
import {useDefaultHeaderHeight} from '@hooks/header';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal, goToScreen, loginAnimationOptions, popTopScreen} from '@screens/navigation';
|
||||
import {preventDoubleTap} from '@utils/tap';
|
||||
|
|
@ -213,6 +214,7 @@ const LoginOptions = ({
|
|||
<View
|
||||
style={styles.flex}
|
||||
testID='login.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea style={[styles.container, transform]}>
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {openAsBottomSheet, popTopScreen, setButtons} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {showRemoveChannelUserSnackbar} from '@utils/snack_bar';
|
||||
|
|
@ -291,6 +292,7 @@ export default function ManageChannelMembers({
|
|||
<SafeAreaView
|
||||
style={styles.container}
|
||||
testID='manage_members.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View style={styles.searchBar}>
|
||||
<Search
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import Loading from '@components/loading';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useAvoidKeyboard} from '@hooks/device';
|
||||
import {t} from '@i18n';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import Background from '@screens/background';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
|
|
@ -168,7 +169,10 @@ const MFA = ({componentId, config, goToHome, license, loginId, password, serverD
|
|||
useAndroidHardwareBackHandler(componentId, close);
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
style={styles.flex}
|
||||
>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea
|
||||
testID='mfa.screen'
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import Animated, {ReduceMotion, useAnimatedStyle, useDerivedValue, useSharedValu
|
|||
|
||||
import {storeOnboardingViewedValue} from '@actions/app/global';
|
||||
import {Screens} from '@constants';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import Background from '@screens/background';
|
||||
import {goToScreen, loginAnimationOptions} from '@screens/navigation';
|
||||
|
||||
|
|
@ -27,8 +28,10 @@ import SlideItem from './slide';
|
|||
import useSlidesData from './slides_data';
|
||||
|
||||
import type {LaunchProps} from '@typings/launch';
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
interface OnboardingProps extends LaunchProps {
|
||||
componentId: AvailableScreens;
|
||||
theme: Theme;
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +52,7 @@ const styles = StyleSheet.create({
|
|||
const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView);
|
||||
|
||||
const Onboarding = ({
|
||||
componentId,
|
||||
theme,
|
||||
...props
|
||||
}: OnboardingProps) => {
|
||||
|
|
@ -133,6 +137,7 @@ const Onboarding = ({
|
|||
<View
|
||||
style={styles.onBoardingContainer}
|
||||
testID='onboarding.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 = {
|
||||
componentId: AvailableScreens;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Overlay = ({children}: Props) => {
|
||||
return children;
|
||||
const Overlay = ({children, componentId}: Props) => {
|
||||
return (<View nativeID={SecurityManager.getShieldScreenId(componentId)}>{children}</View>);
|
||||
};
|
||||
|
||||
export default Overlay;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {useTheme} from '@context/theme';
|
|||
import DatabaseManager from '@database/manager';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {getChannelById, getMyChannel} from '@queries/servers/channel';
|
||||
import {dismissModal} from '@screens/navigation';
|
||||
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
|
||||
|
|
@ -355,6 +356,7 @@ function Permalink({
|
|||
<SafeAreaView
|
||||
style={containerStyle}
|
||||
testID='permalink.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(Screens.PERMALINK)}
|
||||
edges={edges}
|
||||
>
|
||||
<Animated.View style={style.wrapper}>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {ExtraKeyboardProvider} from '@context/extra_keyboard';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
import {getDateForDateLine, selectOrderedPosts} from '@utils/post_list';
|
||||
|
||||
|
|
@ -152,6 +153,7 @@ function SavedMessages({
|
|||
edges={edges}
|
||||
style={styles.flex}
|
||||
testID='pinned_messages.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<ExtraKeyboardProvider>
|
||||
<FlatList
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useBackNavigation from '@hooks/navigate_back';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissOverlay, showShareFeedbackOverlay} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -168,7 +169,10 @@ const ReviewApp = ({
|
|||
}), []);
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.root}
|
||||
>
|
||||
<View
|
||||
style={styles.container}
|
||||
testID='rate_app.screen'
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {addCurrentUserToTeam, fetchTeamsForComponent, handleTeamChange} from '@a
|
|||
import Loading from '@components/loading';
|
||||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {logDebug} from '@utils/log';
|
||||
import {alertTeamAddError} from '@utils/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
|
|
@ -21,6 +22,8 @@ import Header from './header';
|
|||
import NoTeams from './no_teams';
|
||||
import TeamList from './team_list';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
||||
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
||||
container: {
|
||||
flex: 1,
|
||||
|
|
@ -34,6 +37,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
|
|||
}));
|
||||
|
||||
type Props = {
|
||||
componentId: AvailableScreens;
|
||||
nTeams: number;
|
||||
firstTeamId?: string;
|
||||
}
|
||||
|
|
@ -42,6 +46,7 @@ const safeAreaEdges = ['left' as const, 'right' as const];
|
|||
const safeAreaStyle = {flex: 1};
|
||||
|
||||
const SelectTeam = ({
|
||||
componentId,
|
||||
nTeams,
|
||||
firstTeamId,
|
||||
}: Props) => {
|
||||
|
|
@ -140,6 +145,7 @@ const SelectTeam = ({
|
|||
mode='margin'
|
||||
edges={safeAreaEdges}
|
||||
style={safeAreaStyle}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<Animated.View style={top}/>
|
||||
<View style={styles.container}>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {t} from '@i18n';
|
|||
import {getServerCredentials} from '@init/credentials';
|
||||
import PushNotifications from '@init/push_notifications';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {getServerByDisplayName, getServerByIdentifier} from '@queries/app/servers';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal, goToScreen, loginAnimationOptions, popTopScreen} from '@screens/navigation';
|
||||
|
|
@ -340,6 +341,22 @@ const Server = ({
|
|||
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 credentials = await getServerCredentials(ping.url);
|
||||
setConnecting(false);
|
||||
|
|
@ -367,6 +384,7 @@ const Server = ({
|
|||
<View
|
||||
style={styles.flex}
|
||||
testID='server.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {useServerUrl} from '@context/server';
|
|||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useBackNavigation from '@hooks/navigate_back';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissOverlay} from '@screens/navigation';
|
||||
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -134,7 +135,10 @@ const ShareFeedback = ({
|
|||
}), []);
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
style={styles.root}
|
||||
>
|
||||
<View
|
||||
style={styles.container}
|
||||
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 {useTheme} from '@context/theme';
|
||||
import {useIsTablet} from '@hooks/device';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissOverlay} from '@screens/navigation';
|
||||
import {makeStyleSheetFromTheme} from '@utils/theme';
|
||||
import {typography} from '@utils/typography';
|
||||
|
|
@ -261,6 +262,7 @@ const SnackBar = ({
|
|||
<GestureDetector gesture={gesture}>
|
||||
<Animated.View
|
||||
style={animatedMotion}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<Animated.View
|
||||
entering={FadeIn.duration(300)}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {Screens, Sso} from '@constants';
|
|||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useNavButtonPressed from '@hooks/navigation_button_pressed';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import Background from '@screens/background';
|
||||
import {dismissModal, popTopScreen, resetToHome} from '@screens/navigation';
|
||||
import {getFullErrorMessage, isErrorWithUrl} from '@utils/errors';
|
||||
|
|
@ -172,7 +173,10 @@ const SSO = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<View style={styles.flex}>
|
||||
<View
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId, false, true)}
|
||||
style={styles.flex}
|
||||
>
|
||||
<Background theme={theme}/>
|
||||
<AnimatedSafeArea style={[styles.flex, transform]}>
|
||||
{authentication}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
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 useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen} from '@screens/navigation';
|
||||
|
||||
import type {AvailableScreens} from '@typings/screens/navigation';
|
||||
|
|
@ -48,15 +49,20 @@ const Table = ({componentId, renderAsFlex, renderRows, width}: Props) => {
|
|||
|
||||
if (Platform.OS === 'android') {
|
||||
return (
|
||||
<ScrollView testID='table.screen'>
|
||||
<ScrollView
|
||||
contentContainerStyle={viewStyle}
|
||||
horizontal={true}
|
||||
testID='table.scroll_view'
|
||||
>
|
||||
{content}
|
||||
<View
|
||||
style={styles.container}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<ScrollView testID='table.screen'>
|
||||
<ScrollView
|
||||
contentContainerStyle={viewStyle}
|
||||
horizontal={true}
|
||||
testID='table.scroll_view'
|
||||
>
|
||||
{content}
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +70,7 @@ const Table = ({componentId, renderAsFlex, renderRows, width}: Props) => {
|
|||
<SafeAreaView
|
||||
style={styles.container}
|
||||
testID='table.screen'
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<ScrollView
|
||||
style={styles.fullHeight}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import {Screens} from '@constants/index';
|
|||
import {useServerUrl} from '@context/server';
|
||||
import {useTheme} from '@context/theme';
|
||||
import useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {dismissOverlay} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
|
||||
|
|
@ -291,7 +292,10 @@ const TermsOfService = ({
|
|||
}, [styles, insets]);
|
||||
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
<View
|
||||
style={styles.root}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<View style={containerStyle}>
|
||||
<View style={styles.wrapper}>
|
||||
<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 useAndroidHardwareBackHandler from '@hooks/android_back_handler';
|
||||
import useDidUpdate from '@hooks/did_update';
|
||||
import SecurityManager from '@managers/security_manager';
|
||||
import {popTopScreen, setButtons} from '@screens/navigation';
|
||||
import EphemeralStore from '@store/ephemeral_store';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
|
|
@ -114,6 +115,7 @@ const Thread = ({
|
|||
edges={edges}
|
||||
testID='thread.screen'
|
||||
onLayout={onLayout}
|
||||
nativeID={SecurityManager.getShieldScreenId(componentId)}
|
||||
>
|
||||
<RoundedHeaderContext/>
|
||||
{Boolean(rootPost) &&
|
||||
|
|
|
|||
|
|
@ -702,14 +702,20 @@
|
|||
"mobile.manage_members.remove_member": "Remove from Channel",
|
||||
"mobile.manage_members.section_title_admins": "CHANNEL ADMINS",
|
||||
"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.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.ios": "This device must be secured with a passcode to use Mattermost.\n\nGo to Settings > Face ID & Passcode.",
|
||||
"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.android.vendor": "This device must be secured with a screen lock to use {vendor}.",
|
||||
"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.settings": "Go to settings",
|
||||
"mobile.managed.switch_server": "Switch server",
|
||||
"mobile.markdown.code.copy_code": "Copy Code",
|
||||
"mobile.markdown.code.plusMoreLines": "+{count, number} more {count, plural, one {line} other {lines}}",
|
||||
"mobile.markdown.copy_header": "Copy header text",
|
||||
|
|
|
|||
|
|
@ -8,9 +8,14 @@
|
|||
|
||||
import Foundation
|
||||
import Gekidou
|
||||
import react_native_emm
|
||||
|
||||
@objc class GekidouWrapper: NSObject {
|
||||
@objc public static let `default` = GekidouWrapper()
|
||||
|
||||
override init() {
|
||||
ScreenCaptureManager.startTrackingScreens()
|
||||
}
|
||||
|
||||
@objc func postNotificationReceipt(_ userInfo: [AnyHashable:Any]) {
|
||||
PushNotification.default.postNotificationReceipt(userInfo)
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ static SendReplyCompletionHandlerIMP originalSendReplyCompletionHandlerImplement
|
|||
|
||||
if (serverUrl == nil) {
|
||||
[self handleReplyFailure:@"" completionHandler:notificationCompletionHandler];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *sessionToken = [[Keychain default] getTokenObjcFor:serverUrl];
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ static URLSessionTaskDidReceiveChallengeIMP originalURLSessionTaskDidReceiveChal
|
|||
delegateQueue:session.delegateQueue];
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ PODS:
|
|||
- DoubleConversion (1.1.6)
|
||||
- EXApplication (6.0.1):
|
||||
- ExpoModulesCore
|
||||
- EXConstants (17.0.5):
|
||||
- EXConstants (17.0.3):
|
||||
- ExpoModulesCore
|
||||
- Expo (52.0.19):
|
||||
- ExpoModulesCore
|
||||
|
|
@ -15,7 +15,7 @@ PODS:
|
|||
- ExpoModulesCore
|
||||
- ExpoDevice (7.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoFileSystem (18.0.7):
|
||||
- ExpoFileSystem (18.0.6):
|
||||
- ExpoModulesCore
|
||||
- ExpoImage (2.0.4):
|
||||
- ExpoModulesCore
|
||||
|
|
@ -1410,7 +1410,7 @@ PODS:
|
|||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- react-native-emm (1.5.1):
|
||||
- react-native-emm (1.6.0):
|
||||
- DoubleConversion
|
||||
- glog
|
||||
- hermes-engine
|
||||
|
|
@ -2496,11 +2496,11 @@ SPEC CHECKSUMS:
|
|||
CocoaLumberjack: 6a459bc897d6d80bd1b8c78482ec7ad05dffc3f0
|
||||
DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
|
||||
EXApplication: 88ebf1a95f85faa5d2df160016d61f2d060e9438
|
||||
EXConstants: e25c3f3ef5d1cf7a6ef911ebf7a3ab94d8d06d82
|
||||
EXConstants: 277129d9a42ba2cf1fad375e7eaa9939005c60be
|
||||
Expo: 848501c300b37b3fa7330c58a5353e07e03d5c3d
|
||||
ExpoCrypto: 483fc758365923f89ddaab4327c21cae9534841d
|
||||
ExpoDevice: 449822f2c45660c1ce83283dd51c67fe5404996c
|
||||
ExpoFileSystem: 818e82dbb71175414d1ca310e926c48ff0d07348
|
||||
ExpoFileSystem: a38e1bb58d77f41717293a7b73ebd4014d8cb8dd
|
||||
ExpoImage: 18ec1a3e5cd96ee6162d988be3085e18113e143f
|
||||
ExpoLinearGradient: 18148bd38f98fa0229c1f9df23393b32ab6d2d32
|
||||
ExpoModulesCore: c9cb309323aee3c048099bb40ad0226e3fcf0c56
|
||||
|
|
@ -2550,7 +2550,7 @@ SPEC CHECKSUMS:
|
|||
react-native-cameraroll: 5f180ef5e9b52b6c3c3a2645fa33a921d1e37a6c
|
||||
react-native-cookies: d648ab7025833b977c0b19e142503034f5f29411
|
||||
react-native-document-picker: 530879d9e89b490f0954bcc4ab697c5b5e35d659
|
||||
react-native-emm: f6003bebdf4fef4feef7c61f96a5c174f43c6b5f
|
||||
react-native-emm: 710fa6ce569cf04084224fa61a038e6143ea7fe0
|
||||
react-native-image-picker: 130fad649d07e4eec8faaed361d3bba570e1e5ff
|
||||
react-native-netinfo: cec9c4e86083cb5b6aba0e0711f563e2fbbff187
|
||||
react-native-network-client: fdbd7f5f4c2818d6b90b4dcf9613d0a54ab41303
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// eslint-disable-next-line no-process-env
|
||||
process.env.TZ = 'UTC';
|
||||
|
||||
module.exports = {
|
||||
preset: 'jest-expo',
|
||||
verbose: true,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ class MattermostHardwareKeyboardImpl(reactApplicationContext: ReactApplicationCo
|
|||
}
|
||||
|
||||
private fun sendEvent(action: String) {
|
||||
if (!this::context.isInitialized) {
|
||||
return
|
||||
}
|
||||
|
||||
if (context.hasActiveReactInstance()) {
|
||||
val result: WritableMap = WritableNativeMap()
|
||||
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/compass-icons": "0.1.48",
|
||||
"@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-paste-input": "0.8.1",
|
||||
"@mattermost/react-native-turbo-log": "0.6.0",
|
||||
|
|
@ -183,6 +183,11 @@
|
|||
"version": "0.0.0",
|
||||
"license": "Apache 2.0"
|
||||
},
|
||||
"libraries/@mattermost/rnshield": {
|
||||
"version": "0.0.0",
|
||||
"extraneous": true,
|
||||
"license": "Apache 2.0"
|
||||
},
|
||||
"libraries/@mattermost/rnutils": {
|
||||
"version": "0.0.0",
|
||||
"license": "Apache 2.0"
|
||||
|
|
@ -4633,9 +4638,9 @@
|
|||
"link": true
|
||||
},
|
||||
"node_modules/@mattermost/react-native-emm": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.5.1.tgz",
|
||||
"integrity": "sha512-gkuj+UyPWqfxdG62lcqDT5301tPL5dgRpFm63HHegVs45bJX4LmNDHgF7MJuravOJjKX3huMpJDG/UnzHncbkQ==",
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@mattermost/react-native-emm/-/react-native-emm-1.6.0.tgz",
|
||||
"integrity": "sha512-e8Ka0Zd6LZsbVWIiGeASiydV0WTQqc+2LNl8t/Zm22i0M7zhVgf4QIciXE/XjVJk1tfxJ5fhRM4LCp8q87HgTw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"@mattermost/calls": "github:mattermost/calls-common#030ff7c0a37ee9b0ccc5fae0b2ea9c0e1c08473f",
|
||||
"@mattermost/compass-icons": "0.1.48",
|
||||
"@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-paste-input": "0.8.1",
|
||||
"@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
|
||||
index 72fe2cb..abac348 100644
|
||||
index 0716ecd..081d723 100644
|
||||
--- a/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({
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ jest.mock('@nozbe/watermelondb/utils/common/randomId/randomId', () => ({}));
|
|||
jest.mock('@database/manager');
|
||||
jest.doMock('react-native', () => {
|
||||
const {
|
||||
AppState: RNAppState,
|
||||
Platform,
|
||||
StyleSheet,
|
||||
requireNativeComponent,
|
||||
|
|
@ -87,6 +88,13 @@ jest.doMock('react-native', () => {
|
|||
alert: jest.fn(),
|
||||
};
|
||||
|
||||
const AppState = {
|
||||
...RNAppState,
|
||||
addEventListener: jest.fn(() => ({
|
||||
remove: jest.fn(),
|
||||
})),
|
||||
};
|
||||
|
||||
const InteractionManager = {
|
||||
...RNInteractionManager,
|
||||
runAfterInteractions: jest.fn((cb) => cb()),
|
||||
|
|
@ -214,10 +222,12 @@ jest.doMock('react-native', () => {
|
|||
minor: 64,
|
||||
},
|
||||
},
|
||||
select: jest.fn((dict) => dict.ios || dict.default),
|
||||
},
|
||||
StyleSheet,
|
||||
requireNativeComponent,
|
||||
Alert,
|
||||
AppState,
|
||||
InteractionManager,
|
||||
NativeModules,
|
||||
Linking,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import assert from 'assert';
|
|||
|
||||
import {random} from 'lodash';
|
||||
import nock from 'nock';
|
||||
import {of as of$} from 'rxjs';
|
||||
|
||||
import Config from '@assets/config.json';
|
||||
import {Client} from '@client/rest';
|
||||
|
|
@ -18,6 +19,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {prepareCommonSystemValues} from '@queries/servers/system';
|
||||
|
||||
import type {APIClientInterface} from '@mattermost/react-native-network-client';
|
||||
import type {Model, Query} from '@nozbe/watermelondb';
|
||||
|
||||
const DEFAULT_LOCALE = 'en';
|
||||
|
||||
|
|
@ -731,6 +733,13 @@ class TestHelperSingleton {
|
|||
|
||||
wait = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
|
||||
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();
|
||||
|
|
|
|||
5
types/api/config.d.ts
vendored
5
types/api/config.d.ts
vendored
|
|
@ -149,6 +149,9 @@ interface ClientConfig {
|
|||
MaxNotificationsPerChannel: string;
|
||||
MaxPostSize: string;
|
||||
MinimumHashtagLength: string;
|
||||
MobileEnableBiometrics: string;
|
||||
MobileJailbreakProtection: string;
|
||||
MobilePreventScreenCapture: string;
|
||||
MobileExternalBrowser: string;
|
||||
OpenIdButtonColor: string;
|
||||
OpenIdButtonText: string;
|
||||
|
|
@ -198,3 +201,5 @@ interface ClientConfig {
|
|||
WebsocketSecurePort: string;
|
||||
WebsocketURL: string;
|
||||
}
|
||||
|
||||
type SecurityClientConfig = Pick<ClientConfig, 'MobileEnableBiometrics' | 'MobileJailbreakProtection' | 'MobilePreventScreenCapture' | 'SiteName'>
|
||||
|
|
|
|||
Loading…
Reference in a new issue