Integrate react-native-network-client (#5499)

* fix: handle NSMutableData

* feat: integrate react-native-network-client

* fix: typos

* fix: semicolon

* fix: rename to urlVersion

* fix: add returnDataOnly arg

* fix: configure network client

* fix: headers

* fix: handling of serverVersion

* fix: rename requests to actions

* fix: action imports

* fix: no need to stringify body

* fix: sso flow

* fix: address PR feedback

* fix: invalidate client on logout

* fix: address PR feedback take 2

* fix: address PR feedback take 3

* fix: tsc issues

* fix: get csrf token during client creation

* fix: linter

* fix: invalidate client onLogout

* fix: event emitter

* fix: unit tests

* fix: apply linter fixes

* fix lint

* Modify actions to add / update database values

* Rename clien4.d.ts to client.d.ts

* fix empty & missing translations

* cleanup api client

* Cleanup init & squash some TODO's

* Emit certificate errors in NetworkManager

* cleanup user actions

* Fix NetworkManager invalidate client

* Invalidate client when server screen appears

* Update kotlin to 1.4.30 required by network-client

* patch react-native-keychain to remove cached credential

* update react-native-network-client

* Use app.db instead of default.db in native code

* fix use of rnnc on Android

* Init PushNotifications

* No need to reset serverVersion on logout

* fix logout action

* fix deleteServerDatabase

* fix schedule expired session notification

* use safeParseJSON for db json fields

* unsubscribe when database component unmounts

* cleanup init

* session type

* pass launchprops to entire login flow

* Properly remove third party cookies after SSO login

* recreate network client if sso with redirect fails

* add missing launch props from server screen

* use query prefix for database queries

* Add temporary logout function to channel screen

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Miguel Alatzar 2021-07-06 08:16:35 -07:00 committed by GitHub
parent 7ff119fdc1
commit 134c4a49c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
96 changed files with 2539 additions and 2753 deletions

View file

@ -1528,7 +1528,7 @@ This product contains a modified version of 'react-native-cookies' by Joseph P.
Cookie manager for react native.
* HOMEPAGE:
* https://github.com/joeferraro/react-native-cookies
* https://github.com/react-native-cookies/cookies
* LICENSE: MIT

View file

@ -210,6 +210,15 @@ configurations.all {
if (details.requested.name == 'play-services-basement') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '15.0.1'
}
if (details.requested.name == 'okhttp') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.1'
}
if (details.requested.name == 'okhttp-tls') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.1'
}
if (details.requested.name == 'okhttp-urlconnection') {
details.useTarget group: details.requested.group, name: details.requested.name, version: '4.9.1'
}
}
}
}

View file

@ -7,7 +7,7 @@ import android.net.Uri;
import com.nozbe.watermelondb.Database;
public class DatabaseHelper {
private static final String DEFAULT_DATABASE_NAME = "default.db";
private static final String DEFAULT_DATABASE_NAME = "app.db";
private static Database defaultDatabase;
private static void setDefaultDatabase(Context context) {
@ -21,7 +21,7 @@ public class DatabaseHelper {
}
String emptyArray[] = {};
String query = "SELECT url FROM servers";
String query = "SELECT url FROM Servers";
Cursor cursor = defaultDatabase.rawQuery(query, emptyArray);
if (cursor.getCount() == 1) {

View file

@ -42,11 +42,13 @@ import com.facebook.react.bridge.WritableMap;
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.modules.network.OkHttpClientProvider;
import com.facebook.soloader.SoLoader;
import org.unimodules.adapters.react.ModuleRegistryAdapter;
import org.unimodules.adapters.react.ReactModuleRegistryProvider;
import com.mattermost.networkclient.RCTOkHttpClientFactory;
import com.swmansion.reanimated.ReanimatedJSIModulePackage;
public class MainApplication extends NavigationApplication implements INotificationsApplication, INotificationsDrawerApplication {
@ -151,8 +153,10 @@ public class MainApplication extends NavigationApplication implements INotificat
SoLoader.init(this, /* native exopackage */ false);
initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
// Uncomment to listen to react markers for build that has telemetry enabled
// addReactMarkerListener();
// Tells React Native to use our RCTOkHttpClientFactory which builds an OKHttpClient
// with a cookie jar defined in APIClientModule and an interceptor to intercept all
// requests that originate from React Native's OKHttpClient
OkHttpClientProvider.setOkHttpClientFactory(new RCTOkHttpClientFactory());
}
@Override

View file

@ -8,8 +8,8 @@ buildscript {
targetSdkVersion = 29
ndkVersion = "20.1.5948944"
supportLibVersion = "28.0.0"
kotlinVersion = "1.3.61"
kotlin_version = "1.3.61"
kotlinVersion = "1.4.30"
kotlin_version = "1.4.30"
firebaseVersion = "21.0.0"
RNNKotlinVersion = kotlinVersion
}

View file

@ -4,8 +4,8 @@
import {getTimeZone} from 'react-native-localize';
import DatabaseManager from '@database/manager';
import {getUserById} from '@queries/servers/user';
import {updateMe} from '@requests/remote/user';
import {queryUserById} from '@queries/servers/user';
import {updateMe} from '@actions/remote/user';
import {Config} from '@typings/database/models/servers/config';
import User from '@typings/database/models/servers/user';
@ -23,7 +23,7 @@ export const autoUpdateTimezone = async (serverUrl: string, {deviceTimezone, use
return {error: `No database present for ${serverUrl}`};
}
const currentUser = await getUserById({userId, database}) ?? null;
const currentUser = await queryUserById({userId, database}) ?? null;
if (!currentUser) {
return null;

View file

@ -0,0 +1,86 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getServerCredentials} from '@init/credentials';
import NetworkManager from '@init/network_manager';
import type {ClientResponse} from '@mattermost/react-native-network-client';
import type {RawSystem} from '@typings/database/database';
export const doPing = async (serverUrl: string) => {
const client = await NetworkManager.createClient(serverUrl);
const certificateError = {
id: 'mobile.server_requires_client_certificate',
defaultMessage: 'Server required client certificate for authentication.',
};
const pingError = {
id: 'mobile.server_ping_failed',
defaultMessage: 'Cannot connect to the server. Please check your server URL and internet connection.',
};
let response: ClientResponse;
try {
response = await client.ping();
if (response.code === 401) {
// Don't invalidate the client since we want to eventually
// import a certificate with client.importClientP12()
// if for some reason cert is not imported do invalidate the client then.
return {error: {intl: certificateError}};
}
if (!response.ok) {
NetworkManager.invalidateClient(serverUrl);
return {error: {intl: pingError}};
}
} catch (error) {
NetworkManager.invalidateClient(serverUrl);
return {error};
}
return {error: undefined};
};
export const fetchConfigAndLicense = async (serverUrl: string) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
const [config, license] = await Promise.all<ClientConfig, ClientLicense>([
client.getClientConfigOld(),
client.getClientLicenseOld(),
]);
// If we have credentials for this server then update the values in the database
const credentials = await getServerCredentials(serverUrl);
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (credentials && operator) {
const systems: RawSystem[] = [{
id: SYSTEM_IDENTIFIERS.CONFIG,
value: JSON.stringify(config),
}, {
id: SYSTEM_IDENTIFIERS.LICENSE,
value: JSON.stringify(license),
}];
operator.handleSystem({systems, prepareRecordsOnly: false}).
catch((error) => {
// eslint-disable-next-line no-console
console.log('An error ocurred while saving config & license', error);
});
}
return {config, license};
} catch (error) {
return {error};
}
};

View file

@ -4,18 +4,13 @@
import moment from 'moment-timezone';
import {IntlShape} from 'react-intl';
import {Client4} from '@client/rest';
import DatabaseManager from '@database/manager';
import PushNotifications from '@init/push_notifications';
import {getCommonSystemValues} from '@app/queries/servers/system';
import {getSessions} from '@requests/remote/user';
import {queryCommonSystemValues} from '@app/queries/servers/system';
import {getSessions} from '@actions/remote/user';
import {Config} from '@typings/database/models/servers/config';
import {isMinimumServerVersion} from '@utils/helpers';
const MAJOR_VERSION = 5;
const MINOR_VERSION = 24;
const sortByNewest = (a: any, b: any) => {
const sortByNewest = (a: Session, b: Session) => {
if (a.create_at > b.create_at) {
return -1;
}
@ -25,32 +20,32 @@ const sortByNewest = (a: any, b: any) => {
export const scheduleExpiredNotification = async (serverUrl: string, intl: IntlShape) => {
const database = DatabaseManager.serverDatabases[serverUrl].database;
const {currentUserId, config}: {currentUserId: string, config: Partial<Config>} = await getCommonSystemValues(database);
const {currentUserId, config}: {currentUserId: string, config: Partial<Config>} = await queryCommonSystemValues(database);
if (isMinimumServerVersion(Client4.serverVersion, MAJOR_VERSION, MINOR_VERSION) && config.ExtendSessionLengthWithActivity === 'true') {
if (config.ExtendSessionLengthWithActivity === 'true') {
PushNotifications.cancelAllLocalNotifications();
return null;
}
const timeOut = setTimeout(async () => {
clearTimeout(timeOut);
let sessions: any;
let sessions: Session[]|undefined;
try {
sessions = await getSessions(serverUrl, currentUserId);
} catch (e) {
// console.warn('Failed to get current session', e);
// eslint-disable-next-line no-console
console.warn('Failed to get user sessions', e);
return;
}
if (!Array.isArray(sessions?.data)) {
if (!Array.isArray(sessions)) {
return;
}
const session = sessions.data.sort(sortByNewest)[0];
const expiresAt = session?.expires_at || 0; //eslint-disable-line camelcase
const expiresInDays = parseInt(String(Math.ceil(Math.abs(moment.duration(moment().diff(expiresAt)).asDays()))), 10);
const session = sessions.sort(sortByNewest)[0];
const expiresAt = session?.expires_at ? parseInt(session.expires_at as string, 10) : 0;
const expiresInDays = Math.ceil(Math.abs(moment.duration(moment().diff(moment(expiresAt))).asDays()));
const message = intl.formatMessage(
{
id: 'mobile.session_expired',
@ -62,17 +57,18 @@ export const scheduleExpiredNotification = async (serverUrl: string, intl: IntlS
);
if (expiresAt) {
//@ts-expect-error: Does not need to set all Notification properties
PushNotifications.scheduleNotification({
fireDate: expiresAt,
body: message,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
userInfo: {
local: true,
payload: {
userInfo: {
local: true,
},
},
});
}
}, 20000);
}, 1000);
return null;
};

View file

@ -1,14 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
import DatabaseManager from '@database/manager';
import {getRoles} from '@app/queries/servers/role';
import NetworkManager from '@init/network_manager';
import {queryRoles} from '@queries/servers/role';
export const loadRolesIfNeeded = async (serverUrl: string, updatedRoles: string[]) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
const database = DatabaseManager.serverDatabases[serverUrl].database;
const operator = DatabaseManager.serverDatabases[serverUrl].operator;
const existingRoles = ((await getRoles(database)) as unknown) as Role[];
const existingRoles = await queryRoles(database);
const roleNames = existingRoles.map((role) => {
return role.name;
@ -19,7 +26,7 @@ export const loadRolesIfNeeded = async (serverUrl: string, updatedRoles: string[
});
try {
const roles = await Client4.getRolesByNames(newRoles);
const roles = await client.getRolesByNames(newRoles);
await operator.handleRole({
roles,

View file

@ -0,0 +1,45 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {logError} from '@actions/remote/error';
import {forceLogoutIfNecessary} from '@actions/remote/user';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import NetworkManager from '@init/network_manager';
import type {RawSystem} from '@typings/database/database';
export const getDataRetentionPolicy = async (serverUrl: string) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
let data = {};
try {
data = await client.getDataRetentionPolicy();
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
logError(error);
return {error};
}
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (operator) {
const systems: RawSystem[] = [{
id: SYSTEM_IDENTIFIERS.DATA_RETENTION_POLICIES,
value: JSON.stringify(data),
}];
operator.handleSystem({systems, prepareRecordsOnly: false}).
catch((error) => {
// eslint-disable-next-line no-console
console.log('An error ocurred while saving data retention policies', error);
});
}
return data;
};

View file

@ -1,65 +1,68 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
import {DeviceEventEmitter} from 'react-native';
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@actions/local/timezone';
import {logError} from '@actions/remote/error';
import {loadRolesIfNeeded} from '@actions/remote/role';
import {getDataRetentionPolicy} from '@actions/remote/systems';
import {Database, General} from '@constants';
import DatabaseManager from '@database/manager';
import analytics from '@init/analytics';
import {setServerCredentials} from '@init/credentials';
import {getDeviceToken} from '@app/queries/app/global';
import {getCommonSystemValues, getCurrentUserId} from '@app/queries/servers/system';
import {createSessions} from '@requests/local/systems';
import {autoUpdateTimezone, getDeviceTimezone, isTimezoneEnabled} from '@requests/local/timezone';
import {logError} from '@requests/remote/error';
import {loadRolesIfNeeded} from '@requests/remote/role';
import {getDataRetentionPolicy} from '@requests/remote/systems';
import {Client4Error} from '@typings/api/client4';
import {Config} from '@typings/database/models/servers/config';
import {
LoadMeArgs,
LoginArgs,
RawMyTeam,
RawPreference,
RawRole,
RawTeam,
RawTeamMembership,
RawUser,
} from '@typings/database/database';
import {License} from '@typings/database/models/servers/license';
import Role from '@typings/database/models/servers/role';
import User from '@typings/database/models/servers/user';
import NetworkManager from '@init/network_manager';
import {queryDeviceToken} from '@queries/app/global';
import {queryCommonSystemValues, queryCurrentUserId} from '@queries/servers/system';
import {getCSRFFromCookie} from '@utils/security';
import type {Client4Error} from '@typings/api/client';
import type {Config} from '@typings/database/models/servers/config';
import type {LoadMeArgs, LoginArgs, RawMyTeam, RawPreference,
RawRole, RawTeam, RawTeamMembership, RawUser} from '@typings/database/database';
import type {License} from '@typings/database/models/servers/license';
import type Role from '@typings/database/models/servers/role';
import type User from '@typings/database/models/servers/user';
type LoadedUser = {
currentUser?: RawUser,
error?: Client4Error
}
const HTTP_UNAUTHORIZED = 401;
// TODO: Requests should know the server url
// To select the right DB & Client
export const logout = async (serverUrl: string, skipServerLogout = false) => {
return async () => {
if (!skipServerLogout) {
try {
await Client4.logout();
} catch {
// Do nothing
}
}
//fixme: uncomment below EventEmitter.emit
// EventEmitter.emit(NavigationTypes.NAVIGATION_RESET);
DatabaseManager.deleteServerDatabase(serverUrl);
return {data: true};
};
};
export const forceLogoutIfNecessary = async (serverUrl: string, err: Client4Error) => {
const database = DatabaseManager.serverDatabases[serverUrl].database;
export const completeLogin = async (serverUrl: string, user: RawUser) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
const currentUserId = await getCurrentUserId(database);
const {config, license}: { config: Partial<Config>; license: Partial<License>; } = await queryCommonSystemValues(database);
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
return null;
}
// Set timezone
if (isTimezoneEnabled(config)) {
const timezone = getDeviceTimezone();
await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id});
}
// Data retention
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
getDataRetentionPolicy(serverUrl);
}
return null;
};
export const forceLogoutIfNecessary = async (serverUrl: string, err: Client4Error) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
const currentUserId = await queryCurrentUserId(database);
if ('status_code' in err && err.status_code === HTTP_UNAUTHORIZED && err?.url?.indexOf('/login') === -1 && currentUserId) {
await logout(serverUrl);
@ -68,24 +71,49 @@ export const forceLogoutIfNecessary = async (serverUrl: string, err: Client4Erro
return {error: null};
};
export const getSessions = async (serverUrl: string, currentUserId: string) => {
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch {
return undefined;
}
try {
return await client.getSessions(currentUserId);
} catch (e) {
logError(e);
await forceLogoutIfNecessary(serverUrl, e);
}
return undefined;
};
export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaToken, password}: LoginArgs) => {
let deviceToken;
let user;
let user: RawUser;
const appDatabase = DatabaseManager.appDatabase?.database;
if (!appDatabase) {
return {error: 'App database not found.'};
}
let client;
try {
deviceToken = await getDeviceToken(appDatabase);
user = ((await Client4.login(
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
deviceToken = await queryDeviceToken(appDatabase);
user = (await client.login(
loginId,
password,
mfaToken,
deviceToken,
ldapOnly,
)) as unknown) as RawUser;
) as unknown) as RawUser;
await DatabaseManager.createServerDatabase({
config: {
@ -94,7 +122,8 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
},
});
await DatabaseManager.setActiveServerDatabase(serverUrl);
await getCSRFFromCookie(serverUrl);
const csrfToken = await getCSRFFromCookie(serverUrl);
client.setCSRFToken(csrfToken);
} catch (e) {
return {error: e};
}
@ -105,24 +134,31 @@ export const login = async (serverUrl: string, {ldapOnly = false, loginId, mfaTo
await completeLogin(serverUrl, user);
}
return {result};
return {error: undefined};
};
export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs) => {
let currentUser = user;
const database = DatabaseManager.serverDatabases[serverUrl].database;
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
try {
if (deviceToken) {
await Client4.attachDevice(deviceToken);
await client.attachDevice(deviceToken);
}
if (!currentUser) {
currentUser = ((await Client4.getMe()) as unknown) as RawUser;
currentUser = (await client.getMe() as unknown) as RawUser;
}
} catch (e) {
await forceLogoutIfNecessary(serverUrl, e);
@ -133,20 +169,20 @@ export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs)
}
try {
const analyticsClient = analytics.create(Client4.getUrl());
const analyticsClient = analytics.create(serverUrl);
analyticsClient.setUserId(currentUser.id);
analyticsClient.setUserRoles(currentUser.roles);
//todo: Ask for a unified endpoint that will serve all those values in one go.( while ensuring backward-compatibility through fallbacks to previous code-path)
const teamsRequest = Client4.getMyTeams();
const teamsRequest = client.getMyTeams();
// Goes into myTeam table
const teamMembersRequest = Client4.getMyTeamMembers();
const teamUnreadRequest = Client4.getMyTeamUnreads();
const teamMembersRequest = client.getMyTeamMembers();
const teamUnreadRequest = client.getMyTeamUnreads();
const preferencesRequest = Client4.getMyPreferences();
const configRequest = Client4.getClientConfigOld();
const licenseRequest = Client4.getClientLicenseOld();
const preferencesRequest = client.getMyPreferences();
const configRequest = client.getClientConfigOld();
const licenseRequest = client.getClientLicenseOld();
const [
teams,
@ -181,21 +217,17 @@ export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs)
const systemRecords = operator.handleSystem({
systems: [
{
id: 'config',
id: Database.SYSTEM_IDENTIFIERS.CONFIG,
value: JSON.stringify(config),
},
{
id: 'license',
id: Database.SYSTEM_IDENTIFIERS.LICENSE,
value: JSON.stringify(license),
},
{
id: 'currentUserId',
id: Database.SYSTEM_IDENTIFIERS.CURRENT_USER_ID,
value: currentUser.id,
},
{
id: 'url',
value: Client4.getUrl(),
},
],
prepareRecordsOnly: true,
});
@ -223,7 +255,7 @@ export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs)
let rolesRecords: Role[] = [];
if (rolesToLoad.size > 0) {
const rolesByName = ((await Client4.getRolesByNames(Array.from(rolesToLoad))) as unknown) as RawRole[];
const rolesByName = (await client.getRolesByNames(Array.from(rolesToLoad)) as unknown) as RawRole[];
if (rolesByName?.length) {
rolesRecords = await operator.handleRole({prepareRecordsOnly: true, roles: rolesByName}) as Role[];
@ -243,109 +275,41 @@ export const loadMe = async (serverUrl: string, {deviceToken, user}: LoadMeArgs)
return {currentUser, error: undefined};
};
export const completeLogin = async (serverUrl: string, user: RawUser) => {
const database = DatabaseManager.serverDatabases[serverUrl].database;
if (!database) {
return {error: `${serverUrl} database not found`};
export const logout = async (serverUrl: string, skipServerLogout = false) => {
if (!skipServerLogout) {
try {
const client = NetworkManager.getClient(serverUrl);
await client.logout();
} catch (error) {
// We want to log the user even if logging out from the server failed
// eslint-disable-next-line no-console
console.warn('An error ocurred loging out from the server', serverUrl, error);
}
}
const {config, license}: { config: Partial<Config>; license: Partial<License>; } = await getCommonSystemValues(database);
DeviceEventEmitter.emit(General.SERVER_LOGOUT, serverUrl);
if (!Object.keys(config)?.length || !Object.keys(license)?.length) {
return null;
}
const token = Client4.getToken();
setServerCredentials(serverUrl, user.id, token);
// Set timezone
if (isTimezoneEnabled(config)) {
const timezone = getDeviceTimezone();
await autoUpdateTimezone(serverUrl, {deviceTimezone: timezone, userId: user.id});
}
let dataRetentionPolicy: any;
const operator = DatabaseManager.serverDatabases[Client4.getUrl()].operator;
// Data retention
if (config?.DataRetentionEnableMessageDeletion === 'true' && license?.IsLicensed === 'true' && license?.DataRetention === 'true') {
dataRetentionPolicy = await getDataRetentionPolicy(serverUrl);
await operator.handleSystem({systems: [{id: 'dataRetentionPolicy', value: dataRetentionPolicy}], prepareRecordsOnly: false});
}
return null;
return {data: true};
};
export const updateMe = async (serverUrl: string, user: User) => {
const database = DatabaseManager.serverDatabases[serverUrl].database;
const operator = DatabaseManager.serverDatabases[serverUrl].operator;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let data;
try {
data = ((await Client4.patchMe(user._raw)) as unknown) as RawUser;
} catch (e) {
logError(e);
return {error: e};
}
const systemRecords = operator.handleSystem({
systems: [
{id: 'currentUserId', value: data.id},
{id: 'locale', value: data?.locale},
],
prepareRecordsOnly: true,
});
const userRecord = operator.handleUsers({prepareRecordsOnly: true, users: [data]});
//todo: ?? Do we need to write to TOS table ? See app/mm-redux/reducers/entities/users.ts/profiles/line 152 const
// tosRecords = await DataOperator.handleIsolatedEntity({ tableName: TERMS_OF_SERVICE, values: [{}], });
const models = await Promise.all([
systemRecords,
userRecord,
// ...tosRecords,
]);
if (models?.length) {
await operator.batchRecords(models.flat());
}
const updatedRoles: string[] = data.roles.split(' ');
if (updatedRoles.length) {
await loadRolesIfNeeded(serverUrl, updatedRoles);
}
return {data};
};
export const getSessions = async (serverUrl: string, currentUserId: string) => {
try {
const sessions = await Client4.getSessions(currentUserId);
await createSessions(serverUrl, sessions);
} catch (e) {
logError(e);
await forceLogoutIfNecessary(serverUrl, e);
}
};
type LoadedUser = {
currentUser?: RawUser,
error?: Client4Error
}
export const ssoLogin = async (serverUrl: string) => {
export const ssoLogin = async (serverUrl: string, bearerToken: string, csrfToken: string) => {
let deviceToken;
const database = DatabaseManager.appDatabase?.database;
if (!database) {
return {error: 'App database not found'};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
client.setBearerToken(bearerToken);
client.setCSRFToken(csrfToken);
// Setting up active database for this SSO login flow
try {
await DatabaseManager.createServerDatabase({
@ -355,7 +319,7 @@ export const ssoLogin = async (serverUrl: string) => {
},
});
await DatabaseManager.setActiveServerDatabase(serverUrl);
deviceToken = await getDeviceToken(database);
deviceToken = await queryDeviceToken(database);
} catch (e) {
return {error: e};
}
@ -363,28 +327,67 @@ export const ssoLogin = async (serverUrl: string) => {
let result;
try {
result = await loadMe(serverUrl, {deviceToken}) as unknown as LoadedUser;
result = (await loadMe(serverUrl, {deviceToken}) as unknown) as LoadedUser;
if (!result?.error && result?.currentUser) {
await completeLogin(serverUrl, result.currentUser);
}
} catch (e) {
return {error: e};
return {error: undefined};
}
return result;
};
export const sendPasswordResetEmail = async (email: string) => {
let data;
export const sendPasswordResetEmail = async (serverUrl: string, email: string) => {
let client;
try {
data = await Client4.sendPasswordResetEmail(email);
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
let response;
try {
response = await client.sendPasswordResetEmail(email);
} catch (e) {
return {
error: e,
};
}
return {
data,
data: response.data,
error: undefined,
};
};
export const updateMe = async (serverUrl: string, user: User) => {
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!database) {
return {error: `${serverUrl} database not found`};
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (error) {
return {error};
}
let data;
try {
data = (await client.patchMe(user._raw) as unknown) as RawUser;
} catch (e) {
logError(e);
return {error: e};
}
operator.handleUsers({prepareRecordsOnly: false, users: [data]});
const updatedRoles: string[] = data.roles.split(' ');
if (updatedRoles.length) {
await loadRolesIfNeeded(serverUrl, updatedRoles);
}
return {data};
};

View file

@ -21,7 +21,7 @@ const ClientApps = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getAppsProxyRoute()}/api/v1/call`,
{method: 'post', body: JSON.stringify(callCopy)},
{method: 'post', body: callCopy},
);
}

View file

@ -1,143 +1,82 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ClientOptions} from '@typings/api/client4';
import urlParse from 'url-parse';
import {DeviceEventEmitter} from 'react-native';
import {General} from '@constants';
import {t} from '@i18n';
import {Analytics, create} from '@init/analytics';
import {setServerCredentials} from '@init/credentials';
import type {
APIClientInterface,
ClientHeaders,
ClientResponse,
RequestOptions,
} from '@mattermost/react-native-network-client';
import type {ClientOptions} from '@typings/api/client';
import * as ClientConstants from './constants';
import ClientError from './error';
export default class ClientBase {
analytics: Analytics|undefined;
clusterId = '';
csrf = '';
defaultHeaders: {[x: string]: string} = {};
diagnosticId = '';
enableLogging = false;
includeCookies = true;
logToConsole = false;
managedConfig: any = null;
apiClient: APIClientInterface;
csrfToken = '';
requestHeaders: {[x: string]: string} = {};
serverVersion = '';
token = '';
translations = {
connectionError: 'There appears to be a problem with your internet connection.',
unknownError: 'We received an unexpected status code from the server.',
};
userAgent: string|null = null;
url = '';
urlVersion = '/api/v4';
getAbsoluteUrl(baseUrl: string) {
if (typeof baseUrl !== 'string' || !baseUrl.startsWith('/')) {
return baseUrl;
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
this.apiClient = apiClient;
this.analytics = create(serverUrl);
if (bearerToken) {
this.setBearerToken(bearerToken);
}
if (csrfToken) {
this.setCSRFToken(csrfToken);
}
return this.getUrl() + baseUrl;
}
getOptions(options: ClientOptions) {
const newOptions: ClientOptions = {...options};
const headers: {[x: string]: string} = {
[ClientConstants.HEADER_REQUESTED_WITH]: 'XMLHttpRequest',
...this.defaultHeaders,
};
if (this.token) {
headers[ClientConstants.HEADER_AUTH] = `${ClientConstants.HEADER_BEARER} ${this.token}`;
invalidate() {
if (this.apiClient) {
this.apiClient.invalidate();
}
const csrfToken = this.csrf || '';
if (options.method && options.method.toLowerCase() !== 'get' && csrfToken) {
headers[ClientConstants.HEADER_X_CSRF_TOKEN] = csrfToken;
}
if (this.includeCookies) {
newOptions.credentials = 'include';
}
if (this.userAgent) {
headers[ClientConstants.HEADER_USER_AGENT] = this.userAgent;
}
if (newOptions.headers) {
Object.assign(headers, newOptions.headers);
}
return {
...newOptions,
headers,
};
}
getServerVersion() {
return this.serverVersion;
}
getRequestHeaders(requestMethod: string) {
const headers = {...this.requestHeaders};
getToken() {
return this.token;
}
if (this.csrfToken && requestMethod.toLowerCase() !== 'get') {
headers[ClientConstants.HEADER_X_CSRF_TOKEN] = this.csrfToken;
}
getUrl() {
return this.url;
}
getUrlVersion() {
return this.urlVersion;
return headers;
}
getWebSocketUrl = () => {
return `${this.getBaseRoute()}/websocket`;
return `${this.urlVersion}/websocket`;
}
setAcceptLanguage(locale: string) {
this.defaultHeaders['Accept-Language'] = locale;
this.requestHeaders[ClientConstants.HEADER_ACCEPT_LANGUAGE] = locale;
}
setCSRF(csrfToken: string) {
this.csrf = csrfToken;
setBearerToken(bearerToken: string) {
this.requestHeaders[ClientConstants.HEADER_AUTH] = `${ClientConstants.HEADER_BEARER} ${bearerToken}`;
setServerCredentials(this.apiClient.baseUrl, bearerToken);
}
setDiagnosticId(diagnosticId: string) {
this.diagnosticId = diagnosticId;
}
setEnableLogging(enable: boolean) {
this.enableLogging = enable;
}
setIncludeCookies(include: boolean) {
this.includeCookies = include;
}
setManagedConfig(config: any) {
this.managedConfig = config;
}
setUserAgent(userAgent: string) {
this.userAgent = userAgent;
}
setToken(token: string) {
this.token = token;
}
setUrl(url: string) {
this.url = url.replace(/\/+$/, '');
this.analytics = create(this.url);
setCSRFToken(csrfToken: string) {
this.csrfToken = csrfToken;
}
// Routes
getBaseRoute() {
return `${this.url}${this.urlVersion}`;
}
getUsersRoute() {
return `${this.getBaseRoute()}/users`;
return `${this.urlVersion}/users`;
}
getUserRoute(userId: string) {
@ -145,7 +84,7 @@ export default class ClientBase {
}
getTeamsRoute() {
return `${this.getBaseRoute()}/teams`;
return `${this.urlVersion}/teams`;
}
getTeamRoute(teamId: string) {
@ -165,7 +104,7 @@ export default class ClientBase {
}
getChannelsRoute() {
return `${this.getBaseRoute()}/channels`;
return `${this.urlVersion}/channels`;
}
getChannelRoute(channelId: string) {
@ -181,7 +120,7 @@ export default class ClientBase {
}
getPostsRoute() {
return `${this.getBaseRoute()}/posts`;
return `${this.urlVersion}/posts`;
}
getPostRoute(postId: string) {
@ -189,15 +128,15 @@ export default class ClientBase {
}
getReactionsRoute() {
return `${this.getBaseRoute()}/reactions`;
return `${this.urlVersion}/reactions`;
}
getCommandsRoute() {
return `${this.getBaseRoute()}/commands`;
return `${this.urlVersion}/commands`;
}
getFilesRoute() {
return `${this.getBaseRoute()}/files`;
return `${this.urlVersion}/files`;
}
getFileRoute(fileId: string) {
@ -208,168 +147,104 @@ export default class ClientBase {
return `${this.getUserRoute(userId)}/preferences`;
}
getIncomingHooksRoute() {
return `${this.getBaseRoute()}/hooks/incoming`;
}
getIncomingHookRoute(hookId: string) {
return `${this.getBaseRoute()}/hooks/incoming/${hookId}`;
}
getOutgoingHooksRoute() {
return `${this.getBaseRoute()}/hooks/outgoing`;
}
getOutgoingHookRoute(hookId: string) {
return `${this.getBaseRoute()}/hooks/outgoing/${hookId}`;
}
getOAuthRoute() {
return `${this.url}/oauth`;
}
getOAuthAppsRoute() {
return `${this.getBaseRoute()}/oauth/apps`;
}
getOAuthAppRoute(appId: string) {
return `${this.getOAuthAppsRoute()}/${appId}`;
}
getEmojisRoute() {
return `${this.getBaseRoute()}/emoji`;
return `${this.urlVersion}/emoji`;
}
getEmojiRoute(emojiId: string) {
return `${this.getEmojisRoute()}/${emojiId}`;
}
getBrandRoute() {
return `${this.getBaseRoute()}/brand`;
}
getBrandImageUrl(timestamp: string) {
return `${this.getBrandRoute()}/image?t=${timestamp}`;
}
getDataRetentionRoute() {
return `${this.getBaseRoute()}/data_retention`;
return `${this.urlVersion}/data_retention`;
}
getRolesRoute() {
return `${this.getBaseRoute()}/roles`;
return `${this.urlVersion}/roles`;
}
getTimezonesRoute() {
return `${this.getBaseRoute()}/system/timezones`;
return `${this.urlVersion}/system/timezones`;
}
getRedirectLocationRoute() {
return `${this.getBaseRoute()}/redirect_location`;
}
getBotsRoute() {
return `${this.getBaseRoute()}/bots`;
}
getBotRoute(botUserId: string) {
return `${this.getBotsRoute()}/${botUserId}`;
return `${this.urlVersion}/redirect_location`;
}
getAppsProxyRoute() {
return `${this.url}/plugins/com.mattermost.apps`;
return '/plugins/com.mattermost.apps';
}
// Client Helpers
handleRedirectProtocol = (url: string, response: Response) => {
const serverUrl = this.getUrl();
const parsed = urlParse(url);
if (response.redirected) {
const redirectUrl = urlParse(response.url);
if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) {
this.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
}
doFetch = async (url: string, options: ClientOptions, returnDataOnly = true) => {
let request;
const method = options.method?.toLowerCase();
switch (method) {
case 'get':
request = this.apiClient!.get;
break;
case 'put':
request = this.apiClient!.put;
break;
case 'post':
request = this.apiClient!.post;
break;
case 'patch':
request = this.apiClient!.patch;
break;
case 'delete':
request = this.apiClient!.delete;
break;
default:
throw new ClientError(this.apiClient.baseUrl, {
message: 'Invalid request method',
intl: {
id: t('mobile.request.invalid_request_method'),
defaultMessage: 'Invalid request method',
},
url,
});
}
};
doFetch = async (url: string, options: ClientOptions) => {
const {data} = await this.doFetchWithResponse(url, options);
return data;
};
doFetchWithResponse = async (url: string, options: ClientOptions) => {
const response = await fetch(url, this.getOptions(options));
const headers = parseAndMergeNestedHeaders(response.headers);
let data;
const requestOptions: RequestOptions = {
body: options.body,
headers: this.getRequestHeaders(method),
};
let response: ClientResponse;
try {
data = await response.json();
} catch (err) {
throw new ClientError(this.getUrl(), {
response = await request!(url, requestOptions);
} catch (error) {
throw new ClientError(this.apiClient.baseUrl, {
message: 'Received invalid response from the server.',
intl: {
id: 'mobile.request.invalid_response',
id: t('mobile.request.invalid_response'),
defaultMessage: 'Received invalid response from the server.',
},
url,
});
}
if (headers.has(ClientConstants.HEADER_X_VERSION_ID) && !headers.get('Cache-Control')) {
const serverVersion = headers.get(ClientConstants.HEADER_X_VERSION_ID);
if (serverVersion && this.serverVersion !== serverVersion) {
this.serverVersion = serverVersion;
}
const headers: ClientHeaders = response.headers || {};
const serverVersion = headers[ClientConstants.HEADER_X_VERSION_ID] || headers[ClientConstants.HEADER_X_VERSION_ID.toLowerCase()];
const hasCacheControl = Boolean(headers[ClientConstants.HEADER_CACHE_CONTROL] || headers[ClientConstants.HEADER_CACHE_CONTROL.toLowerCase()]);
if (serverVersion && !hasCacheControl && this.serverVersion !== serverVersion) {
this.serverVersion = serverVersion;
DeviceEventEmitter.emit(General.SERVER_VERSION_CHANGED, {serverUrl: this.apiClient.baseUrl, serverVersion});
}
if (headers.has(ClientConstants.HEADER_X_CLUSTER_ID)) {
const clusterId = headers.get(ClientConstants.HEADER_X_CLUSTER_ID);
if (clusterId && this.clusterId !== clusterId) {
this.clusterId = clusterId;
}
const bearerToken = headers[ClientConstants.HEADER_TOKEN] || headers[ClientConstants.HEADER_TOKEN.toLowerCase()];
if (bearerToken) {
this.setBearerToken(bearerToken);
}
if (response.ok) {
return {
response,
headers,
data,
};
return returnDataOnly ? response.data : response;
}
const msg = data.message || '';
if (this.logToConsole) {
console.error(msg); // eslint-disable-line no-console
}
throw new ClientError(this.getUrl(), {
message: msg,
server_error_id: data.id,
status_code: data.status_code,
throw new ClientError(this.apiClient.baseUrl, {
message: response.data?.message || '',
server_error_id: response.data?.id,
status_code: response.code,
url,
});
};
}
function parseAndMergeNestedHeaders(originalHeaders: any) {
const headers = new Map();
let nestedHeaders = new Map();
originalHeaders.forEach((val: string, key: string) => {
const capitalizedKey = key.replace(/\b[a-z]/g, (l) => l.toUpperCase());
let realVal = val;
if (val && val.match(/\n\S+:\s\S+/)) {
const nestedHeaderStrings = val.split('\n');
realVal = nestedHeaderStrings.shift() as string;
const moreNestedHeaders = new Map(
nestedHeaderStrings.map((h: any) => h.split(/:\s/)),
);
nestedHeaders = new Map([...nestedHeaders, ...moreNestedHeaders]);
}
headers.set(capitalizedKey, realVal);
});
return new Map([...headers, ...nestedHeaders]);
}

View file

@ -1,37 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {buildQueryString} from '@utils/helpers';
export interface ClientBotsMix {
getBot: (botUserId: string) => Promise<Bot>;
getBots: (page?: number, perPage?: number) => Promise<Bot[]>;
getBotsIncludeDeleted: (page?: number, perPage?: number) => Promise<Bot[]>;
}
const PER_PAGE_DEFAULT = 60;
const ClientBots = (superclass: any) => class extends superclass {
getBot = async (botUserId: string) => {
return this.doFetch(
`${this.getBotRoute(botUserId)}`,
{method: 'get'},
);
}
getBots = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getBotsRoute()}${buildQueryString({page, per_page: perPage})}`,
{method: 'get'},
);
}
getBotsIncludeDeleted = async (page = 0, perPage = PER_PAGE_DEFAULT) => {
return this.doFetch(
`${this.getBotsRoute()}${buildQueryString({include_deleted: true, page, per_page: perPage})}`,
{method: 'get'},
);
}
};
export default ClientBots;

View file

@ -60,7 +60,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelsRoute()}`,
{method: 'post', body: JSON.stringify(channel)},
{method: 'post', body: channel},
);
};
@ -69,7 +69,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelsRoute()}/direct`,
{method: 'post', body: JSON.stringify(userIds)},
{method: 'post', body: userIds},
);
};
@ -78,7 +78,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelsRoute()}/group`,
{method: 'post', body: JSON.stringify(userIds)},
{method: 'post', body: userIds},
);
};
@ -105,7 +105,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelRoute(channel.id)}`,
{method: 'put', body: JSON.stringify(channel)},
{method: 'put', body: channel},
);
};
@ -123,7 +123,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelRoute(channelId)}/privacy`,
{method: 'put', body: JSON.stringify({privacy})},
{method: 'put', body: {privacy}},
);
};
@ -132,7 +132,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelRoute(channelId)}/patch`,
{method: 'put', body: JSON.stringify(channelPatch)},
{method: 'put', body: channelPatch},
);
};
@ -141,7 +141,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getChannelMemberRoute(props.channel_id, props.user_id)}/notify_props`,
{method: 'put', body: JSON.stringify(props)},
{method: 'put', body: props},
);
};
@ -232,7 +232,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
getChannelMembersByIds = async (channelId: string, userIds: string[]) => {
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}/ids`,
{method: 'post', body: JSON.stringify(userIds)},
{method: 'post', body: userIds},
);
};
@ -242,7 +242,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
const member = {user_id: userId, channel_id: channelId, post_root_id: postRootId};
return this.doFetch(
`${this.getChannelMembersRoute(channelId)}`,
{method: 'post', body: JSON.stringify(member)},
{method: 'post', body: member},
);
};
@ -273,7 +273,7 @@ const ClientChannels = (superclass: any) => class extends superclass {
const data = {channel_id: channelId, prev_channel_id: prevChannelId};
return this.doFetch(
`${this.getChannelsRoute()}/members/me/view`,
{method: 'post', body: JSON.stringify(data)},
{method: 'post', body: data},
);
};
@ -294,14 +294,14 @@ const ClientChannels = (superclass: any) => class extends superclass {
searchChannels = async (teamId: string, term: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/search`,
{method: 'post', body: JSON.stringify({term})},
{method: 'post', body: {term}},
);
};
searchArchivedChannels = async (teamId: string, term: string) => {
return this.doFetch(
`${this.getTeamRoute(teamId)}/channels/search_archived`,
{method: 'post', body: JSON.stringify({term})},
{method: 'post', body: {term}},
);
};
};

View file

@ -2,12 +2,13 @@
// See LICENSE.txt for license information.
export const HEADER_AUTH = 'Authorization';
export const HEADER_ACCEPT_LANGUAGE = 'Accept-Language';
export const HEADER_BEARER = 'BEARER';
export const HEADER_CACHE_CONTROL = 'Cache-Control';
export const HEADER_REQUESTED_WITH = 'X-Requested-With';
export const HEADER_USER_AGENT = 'User-Agent';
export const HEADER_X_CLUSTER_ID = 'X-Cluster-Id';
export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
export const HEADER_TOKEN = 'Token';
export const HEADER_USER_AGENT = 'User-Agent';
export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
export const HEADER_X_VERSION_ID = 'X-Version-Id';
export const DEFAULT_LIMIT_BEFORE = 30;
export const DEFAULT_LIMIT_AFTER = 30;

View file

@ -84,7 +84,7 @@ const ClientEmojis = (superclass: any) => class extends superclass {
searchCustomEmoji = async (term: string, options = {}) => {
return this.doFetch(
`${this.getEmojisRoute()}/search`,
{method: 'post', body: JSON.stringify({term, ...options})},
{method: 'post', body: {term, ...options}},
);
};

View file

@ -10,7 +10,7 @@ export interface ClientGeneralMix {
ping: () => Promise<any>;
logClientError: (message: string, level?: string) => Promise<any>;
getClientConfigOld: () => Promise<ClientConfig>;
getClientLicenseOld: () => Promise<any>;
getClientLicenseOld: () => Promise<ClientLicense>;
getTimezones: () => Promise<string[]>;
getDataRetentionPolicy: () => Promise<any>;
getRolesByNames: (rolesNames: string[]) => Promise<Role[]>;
@ -20,23 +20,24 @@ export interface ClientGeneralMix {
const ClientGeneral = (superclass: any) => class extends superclass {
getOpenGraphMetadata = async (url: string) => {
return this.doFetch(
`${this.getBaseRoute()}/opengraph`,
{method: 'post', body: JSON.stringify({url})},
`${this.urlVersion}/opengraph`,
{method: 'post', body: {url}},
);
};
ping = async () => {
return this.doFetch(
`${this.getBaseRoute()}/system/ping?time=${Date.now()}`,
`${this.urlVersion}/system/ping?time=${Date.now()}`,
{method: 'get'},
false,
);
};
logClientError = async (message: string, level = 'ERROR') => {
const url = `${this.getBaseRoute()}/logs`;
const url = `${this.urlVersion}/logs`;
if (!this.enableLogging) {
throw new ClientError(this.getUrl(), {
throw new ClientError(this.client.baseUrl, {
message: 'Logging disabled.',
url,
});
@ -44,20 +45,20 @@ const ClientGeneral = (superclass: any) => class extends superclass {
return this.doFetch(
url,
{method: 'post', body: JSON.stringify({message, level})},
{method: 'post', body: {message, level}},
);
};
getClientConfigOld = async () => {
return this.doFetch(
`${this.getBaseRoute()}/config/client?format=old`,
`${this.urlVersion}/config/client?format=old`,
{method: 'get'},
);
};
getClientLicenseOld = async () => {
return this.doFetch(
`${this.getBaseRoute()}/license/client?format=old`,
`${this.urlVersion}/license/client?format=old`,
{method: 'get'},
);
};
@ -79,7 +80,7 @@ const ClientGeneral = (superclass: any) => class extends superclass {
getRolesByNames = async (rolesNames: string[]) => {
return this.doFetch(
`${this.getRolesRoute()}/names`,
{method: 'post', body: JSON.stringify(rolesNames)},
{method: 'post', body: rolesNames},
);
};

View file

@ -16,7 +16,7 @@ export interface ClientGroupsMix {
const ClientGroups = (superclass: any) => class extends superclass {
getGroups = async (filterAllowReference = false, page = 0, perPage = PER_PAGE_DEFAULT, since = 0) => {
return this.doFetch(
`${this.getBaseRoute()}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage, since})}`,
`${this.urlVersion}/groups${buildQueryString({filter_allow_reference: filterAllowReference, page, per_page: perPage, since})}`,
{method: 'get'},
);
};
@ -29,21 +29,21 @@ const ClientGroups = (superclass: any) => class extends superclass {
}
getAllGroupsAssociatedToTeam = async (teamID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
`${this.urlVersion}/teams/${teamID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToChannelsInTeam = async (teamID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/teams/${teamID}/groups_by_channels${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
`${this.urlVersion}/teams/${teamID}/groups_by_channels${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};
getAllGroupsAssociatedToChannel = async (channelID: string, filterAllowReference = false) => {
return this.doFetch(
`${this.getBaseRoute()}/channels/${channelID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
`${this.urlVersion}/channels/${channelID}/groups${buildQueryString({paginate: false, filter_allow_reference: filterAllowReference})}`,
{method: 'get'},
);
};

View file

@ -9,7 +9,7 @@ import ClientError from '@client/rest/error';
import TestHelper from '@test/test_helper';
import {isMinimumServerVersion} from '@utils/helpers';
describe('Client4', () => {
describe('Client', () => {
beforeAll(() => {
if (!nock.isActive()) {
nock.activate();
@ -20,25 +20,18 @@ describe('Client4', () => {
nock.restore();
});
it('should remove trailing slash while trying to setUrl', () => {
const client = TestHelper.createClient();
client.setUrl('http://localhost:8065/');
expect(client.url).toBe('http://localhost:8065');
client.setUrl('http://localhost:8065/company/mattermost/');
expect(client.url).toBe('http://localhost:8065/company/mattermost');
});
describe('doFetchWithResponse', () => {
describe('doFetch', () => {
it('serverVersion should be set from response header', async () => {
const client = TestHelper.createClient();
assert.equal(client.serverVersion, '');
nock(client.getBaseRoute()).
get('/users/me').
reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.0.0.5.0.0.abc123'});
client.apiClient.get.mockReturnValueOnce({
code: 200,
data: {},
headers: {[HEADER_X_VERSION_ID]: '5.0.0.5.0.0.abc123'},
ok: true,
});
await client.getMe();
@ -46,9 +39,12 @@ describe('Client4', () => {
assert.equal(isMinimumServerVersion(client.serverVersion, 5, 0, 0), true);
assert.equal(isMinimumServerVersion(client.serverVersion, 5, 1, 0), false);
nock(client.getBaseRoute()).
get('/users/me').
reply(200, '{}', {[HEADER_X_VERSION_ID]: '5.3.0.5.3.0.abc123'});
client.apiClient.get.mockReturnValueOnce({
code: 200,
data: {},
headers: {[HEADER_X_VERSION_ID]: '5.3.0.5.3.0.abc123'},
ok: true,
});
await client.getMe();

View file

@ -6,7 +6,6 @@ import mix from '@utils/mix';
import {DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID} from './constants';
import ClientApps, {ClientAppsMix} from './apps';
import ClientBase from './base';
import ClientBots, {ClientBotsMix} from './bots';
import ClientChannels, {ClientChannelsMix} from './channels';
import ClientEmojis, {ClientEmojisMix} from './emojis';
import ClientFiles, {ClientFilesMix} from './files';
@ -19,9 +18,10 @@ import ClientTeams, {ClientTeamsMix} from './teams';
import ClientTos, {ClientTosMix} from './tos';
import ClientUsers, {ClientUsersMix} from './users';
import type {APIClientInterface} from '@mattermost/react-native-network-client';
interface Client extends ClientBase,
ClientAppsMix,
ClientBotsMix,
ClientChannelsMix,
ClientEmojisMix,
ClientFilesMix,
@ -37,7 +37,6 @@ interface Client extends ClientBase,
class Client extends mix(ClientBase).with(
ClientApps,
ClientBots,
ClientChannels,
ClientEmojis,
ClientFiles,
@ -49,8 +48,11 @@ class Client extends mix(ClientBase).with(
ClientTeams,
ClientTos,
ClientUsers,
) {}
) {
// eslint-disable-next-line no-useless-constructor
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
super(apiClient, serverUrl, bearerToken, csrfToken);
}
}
const Client4 = new Client();
export {Client4, Client, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID};
export {Client, DEFAULT_LIMIT_AFTER, DEFAULT_LIMIT_BEFORE, HEADER_X_VERSION_ID};

View file

@ -41,7 +41,7 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getCommandsRoute()}/execute`,
{method: 'post', body: JSON.stringify({command, ...commandArgs})},
{method: 'post', body: {command, ...commandArgs}},
);
};
@ -50,15 +50,15 @@ const ClientIntegrations = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getCommandsRoute()}`,
{method: 'post', body: JSON.stringify(command)},
{method: 'post', body: command},
);
};
submitInteractiveDialog = async (data: DialogSubmission) => {
this.analytics.trackAPI('api_interactive_messages_dialog_submitted');
return this.doFetch(
`${this.getBaseRoute()}/actions/dialogs/submit`,
{method: 'post', body: JSON.stringify(data)},
`${this.urlVersion}/actions/dialogs/submit`,
{method: 'post', body: data},
);
};
};

View file

@ -41,7 +41,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getPostsRoute()}`,
{method: 'post', body: JSON.stringify(post)},
{method: 'post', body: post},
);
};
@ -50,7 +50,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getPostRoute(post.id)}`,
{method: 'put', body: JSON.stringify(post)},
{method: 'put', body: post},
);
};
@ -66,7 +66,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getPostRoute(postPatch.id)}/patch`,
{method: 'put', body: JSON.stringify(postPatch)},
{method: 'put', body: postPatch},
);
};
@ -174,7 +174,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getReactionsRoute()}`,
{method: 'post', body: JSON.stringify({user_id: userId, post_id: postId, emoji_name: emojiName})},
{method: 'post', body: {user_id: userId, post_id: postId, emoji_name: emojiName}},
);
};
@ -199,7 +199,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getTeamRoute(teamId)}/posts/search`,
{method: 'post', body: JSON.stringify(params)},
{method: 'post', body: params},
);
};
@ -226,7 +226,7 @@ const ClientPosts = (superclass: any) => class extends superclass {
}
return this.doFetch(
`${this.getPostRoute(postId)}/actions/${encodeURIComponent(actionId)}`,
{method: 'post', body: JSON.stringify(msg)},
{method: 'post', body: msg},
);
};
};

View file

@ -11,7 +11,7 @@ const ClientPreferences = (superclass: any) => class extends superclass {
savePreferences = async (userId: string, preferences: PreferenceType[]) => {
return this.doFetch(
`${this.getPreferencesRoute(userId)}`,
{method: 'put', body: JSON.stringify(preferences)},
{method: 'put', body: preferences},
);
};
@ -25,7 +25,7 @@ const ClientPreferences = (superclass: any) => class extends superclass {
deletePreferences = async (userId: string, preferences: PreferenceType[]) => {
return this.doFetch(
`${this.getPreferencesRoute(userId)}/delete`,
{method: 'post', body: JSON.stringify(preferences)},
{method: 'post', body: preferences},
);
};
};

View file

@ -32,7 +32,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getTeamsRoute()}`,
{method: 'post', body: JSON.stringify(team)},
{method: 'post', body: team},
);
};
@ -50,7 +50,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getTeamRoute(team.id)}`,
{method: 'put', body: JSON.stringify(team)},
{method: 'put', body: team},
);
};
@ -59,7 +59,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getTeamRoute(team.id)}/patch`,
{method: 'put', body: JSON.stringify(team)},
{method: 'put', body: team},
);
};
@ -134,7 +134,7 @@ const ClientTeams = (superclass: any) => class extends superclass {
const member = {user_id: userId, team_id: teamId};
return this.doFetch(
`${this.getTeamMembersRoute(teamId)}`,
{method: 'post', body: JSON.stringify(member)},
{method: 'post', body: member},
);
};

View file

@ -10,13 +10,13 @@ const ClientTos = (superclass: any) => class extends superclass {
updateMyTermsOfServiceStatus = async (termsOfServiceId: string, accepted: boolean) => {
return this.doFetch(
`${this.getUserRoute('me')}/terms_of_service`,
{method: 'post', body: JSON.stringify({termsOfServiceId, accepted})},
{method: 'post', body: {termsOfServiceId, accepted}},
);
}
getTermsOfService = async () => {
return this.doFetch(
`${this.getBaseRoute()}/terms_of_service`,
`${this.urlVersion}/terms_of_service`,
{method: 'get'},
);
}

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {General} from '@constants';
import {buildQueryString, isMinimumServerVersion} from '@utils/helpers';
import {buildQueryString} from '@utils/helpers';
import {PER_PAGE_DEFAULT} from './constants';
@ -34,7 +34,7 @@ export interface ClientUsersMix {
getProfilePictureUrl: (userId: string, lastPictureUpdate: number) => string;
getDefaultProfilePictureUrl: (userId: string) => string;
autocompleteUsers: (name: string, teamId: string, channelId: string, options?: Record<string, any>) => Promise<{users: UserProfile[], out_of_channel?: UserProfile[]}>;
getSessions: (userId: string) => Promise<any>;
getSessions: (userId: string) => Promise<Session[]>;
checkUserMfa: (loginId: string) => Promise<{mfa_required: boolean}>;
attachDevice: (deviceId: string) => Promise<any>;
searchUsers: (term: string, options: any) => Promise<UserProfile[]>;
@ -59,14 +59,14 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString(queryParams)}`,
{method: 'post', body: JSON.stringify(user)},
{method: 'post', body: user},
);
}
patchMe = async (userPatch: Partial<UserProfile>) => {
return this.doFetch(
`${this.getUserRoute('me')}/patch`,
{method: 'put', body: JSON.stringify(userPatch)},
{method: 'put', body: userPatch},
);
}
@ -75,7 +75,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUserRoute(userPatch.id)}/patch`,
{method: 'put', body: JSON.stringify(userPatch)},
{method: 'put', body: userPatch},
);
}
@ -84,7 +84,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUserRoute(user.id)}`,
{method: 'put', body: JSON.stringify(user)},
{method: 'put', body: user},
);
}
@ -111,7 +111,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUsersRoute()}/password/reset/send`,
{method: 'post', body: JSON.stringify({email})},
{method: 'post', body: {email}},
);
}
@ -142,13 +142,14 @@ const ClientUsers = (superclass: any) => class extends superclass {
body.ldap_only = 'true';
}
const {data} = await this.doFetchWithResponse(
const {data} = await this.doFetch(
`${this.getUsersRoute()}/login`,
{
method: 'post',
body: JSON.stringify(body),
body,
headers: {'Cache-Control': 'no-store'},
},
false,
);
return data;
@ -163,9 +164,9 @@ const ClientUsers = (superclass: any) => class extends superclass {
token,
};
const {data} = await this.doFetchWithResponse(
const {data} = await this.doFetch(
`${this.getUsersRoute()}/login`,
{method: 'post', body: JSON.stringify(body)},
{method: 'post', body},
);
return data;
@ -174,17 +175,11 @@ const ClientUsers = (superclass: any) => class extends superclass {
logout = async () => {
this.analytics.trackAPI('api_users_logout');
const {response} = await this.doFetchWithResponse(
const response = await this.doFetch(
`${this.getUsersRoute()}/logout`,
{method: 'post'},
);
if (response.ok) {
this.token = '';
}
this.serverVersion = '';
return response;
};
@ -202,7 +197,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUsersRoute()}/ids${buildQueryString(options)}`,
{method: 'post', body: JSON.stringify(userIds)},
{method: 'post', body: userIds},
);
};
@ -211,7 +206,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUsersRoute()}/usernames`,
{method: 'post', body: JSON.stringify(usernames)},
{method: 'post', body: usernames},
);
};
@ -250,13 +245,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
getProfilesInChannel = async (channelId: string, page = 0, perPage = PER_PAGE_DEFAULT, sort = '') => {
this.analytics.trackAPI('api_profiles_get_in_channel', {channel_id: channelId});
const serverVersion = this.getServerVersion();
let queryStringObj;
if (isMinimumServerVersion(serverVersion, 4, 7)) {
queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
} else {
queryStringObj = {in_channel: channelId, page, per_page: perPage};
}
const queryStringObj = {in_channel: channelId, page, per_page: perPage, sort};
return this.doFetch(
`${this.getUsersRoute()}${buildQueryString(queryStringObj)}`,
{method: 'get'},
@ -268,7 +257,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUsersRoute()}/group_channels`,
{method: 'post', body: JSON.stringify(channelsIds)},
{method: 'post', body: channelsIds},
);
};
@ -351,14 +340,14 @@ const ClientUsers = (superclass: any) => class extends superclass {
checkUserMfa = async (loginId: string) => {
return this.doFetch(
`${this.getUsersRoute()}/mfa`,
{method: 'post', body: JSON.stringify({login_id: loginId})},
{method: 'post', body: {login_id: loginId}},
);
};
attachDevice = async (deviceId: string) => {
return this.doFetch(
`${this.getUsersRoute()}/sessions/device`,
{method: 'put', body: JSON.stringify({device_id: deviceId})},
{method: 'put', body: {device_id: deviceId}},
);
};
@ -367,14 +356,14 @@ const ClientUsers = (superclass: any) => class extends superclass {
return this.doFetch(
`${this.getUsersRoute()}/search`,
{method: 'post', body: JSON.stringify({term, ...options})},
{method: 'post', body: {term, ...options}},
);
};
getStatusesByIds = async (userIds: string[]) => {
return this.doFetch(
`${this.getUsersRoute()}/status/ids`,
{method: 'post', body: JSON.stringify(userIds)},
{method: 'post', body: userIds},
);
};
@ -388,7 +377,7 @@ const ClientUsers = (superclass: any) => class extends superclass {
updateStatus = async (status: UserStatus) => {
return this.doFetch(
`${this.getUserRoute(status.user_id)}/status`,
{method: 'put', body: JSON.stringify(status)},
{method: 'put', body: status},
);
};
};

View file

@ -47,7 +47,23 @@ export const MIGRATION_EVENTS = keyMirror({
MIGRATION_SUCCESS: null,
});
export const SYSTEM_IDENTIFIERS = {
CONFIG: 'config',
CURRENT_CHANNEL_ID: 'currentChannelId',
CURRENT_TEAM_ID: 'currentTeamId',
CURRENT_USER_ID: 'currentUserId',
DATA_RETENTION_POLICIES: 'dataRetentionPolicies',
LICENSE: 'license',
};
export const GLOBAL_IDENTIFIERS = {
DEVICE_TOKEN: 'deviceToken',
MENTION_COUNT: 'mentionCount',
};
export default {
GLOBAL_IDENTIFIERS,
MM_TABLES,
MIGRATION_EVENTS,
SYSTEM_IDENTIFIERS,
};

View file

@ -68,5 +68,5 @@ export default {
DISABLED: 'disabled',
DEFAULT_ON: 'default_on',
DEFAULT_OFF: 'default_off',
REHYDRATED: 'app/REHYDRATED',
SERVER_LOGOUT: 'server_logout',
};

View file

@ -2,29 +2,33 @@
// See LICENSE.txt for license information.
import Attachment from './attachment';
import SSO from './sso';
import Database from './database';
import Device from './device';
import Files from './files';
import General from './general';
import List from './list';
import Navigation from './navigation';
import Network from './network';
import Preferences from './preferences';
import Screens from './screens';
import SSO, {REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV} from './sso';
import View, {Upgrade} from './view';
import WebsocketEvents from './websocket';
export {
Attachment,
SSO,
Database,
Device,
Files,
General,
List,
Navigation,
Network,
Preferences,
REDIRECT_URL_SCHEME,
REDIRECT_URL_SCHEME_DEV,
Screens,
SSO,
Upgrade,
View,
WebsocketEvents,

View file

@ -4,7 +4,6 @@
import keyMirror from '@utils/key_mirror';
const Navigation = keyMirror({
NAVIGATION_RESET: null,
NAVIGATION_CLOSE_MODAL: null,
NAVIGATION_NO_TEAMS: null,
NAVIGATION_ERROR_TEAMS: null,

13
app/constants/network.ts Normal file
View file

@ -0,0 +1,13 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import keyMirror from '@utils/key_mirror';
export const CERTIFICATE_ERRORS = keyMirror({
CLIENT_CERTIFICATE_IMPORT_ERROR: null,
CLIENT_CERTIFICATE_MISSING: null,
});
export default {
CERTIFICATE_ERRORS,
};

View file

@ -3,6 +3,9 @@
import keyMirror from '@utils/key_mirror';
export const REDIRECT_URL_SCHEME = 'mmauth://';
export const REDIRECT_URL_SCHEME_DEV = 'mmauthbeta://';
export default keyMirror({
GITLAB: null,
GOOGLE: null,

View file

@ -55,8 +55,6 @@ const ViewTypes = keyMirror({
POST_DRAFT_SELECTION_CHANGED: null,
COMMENT_DRAFT_SELECTION_CHANGED: null,
NOTIFICATION_IN_APP: null,
SET_POST_DRAFT: null,
SET_COMMENT_DRAFT: null,

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {ComponentType, useState} from 'react';
import React, {ComponentType, useEffect, useState} from 'react';
import {Database} from '@nozbe/watermelondb';
import DatabaseProvider from '@nozbe/watermelondb/DatabaseProvider';
@ -15,30 +15,36 @@ export function withServerDatabase<T>(
Component: ComponentType<T>,
): ComponentType<T> {
return function ServerDatabaseComponent(props) {
const [database, setDatabase] = useState<Database|unknown>();
const [database, setDatabase] = useState<Database|undefined>();
const db = DatabaseManager.appDatabase?.database;
const observer = async (servers: Servers[]) => {
const server = servers.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
const serverDatabase = DatabaseManager.serverDatabases[server.url].database;
if (serverDatabase) {
setDatabase(serverDatabase);
}
const serverDatabase = DatabaseManager.serverDatabases[server.url]?.database;
setDatabase(serverDatabase);
};
db?.collections.
get(SERVERS).
query().
observeWithColumns(['last_active_at']).
subscribe(observer);
useEffect(() => {
const subscription = db?.collections.
get(SERVERS).
query().
observeWithColumns(['last_active_at']).
subscribe(observer);
return () => {
subscription?.unsubscribe();
};
}, []);
if (!database) {
return null;
}
return (
<DatabaseProvider database={(database as Database)}>
<DatabaseProvider
database={(database as Database)}
>
<Component {...props}/>
</DatabaseProvider>
);

View file

@ -22,7 +22,7 @@ import {Channel, ChannelInfo, ChannelMembership, CustomEmoji, Draft, File,
TermsOfService, User,
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import {getActiveServer, getServer} from '@queries/app/servers';
import {queryActiveServer, queryServer} from '@queries/app/servers';
import {deleteIOSDatabase} from '@utils/mattermost_managed';
import {hashCode} from '@utils/security';
@ -184,7 +184,7 @@ class DatabaseManager {
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
if (this.appDatabase?.database) {
const server = await getServer(this.appDatabase.database, serverUrl);
const server = await queryServer(this.appDatabase.database, serverUrl);
return Boolean(server);
}
@ -194,7 +194,7 @@ class DatabaseManager {
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
const server = await queryActiveServer(database);
return server?.url;
}
@ -204,7 +204,7 @@ class DatabaseManager {
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
const server = await queryActiveServer(database);
if (server?.url) {
return this.serverDatabases[server.url].database;
}
@ -230,10 +230,10 @@ class DatabaseManager {
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
const server = await queryServer(database, serverUrl);
if (server) {
database.action(() => {
server.update((record) => {
database.action(async () => {
await server.update((record) => {
record.lastActiveAt = 0;
});
});
@ -247,7 +247,7 @@ class DatabaseManager {
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
const server = await queryServer(database, serverUrl);
if (server) {
database.action(async () => {
await server.destroyPermanently();

View file

@ -21,7 +21,7 @@ import {Channel, ChannelInfo, ChannelMembership, CustomEmoji, Draft, File,
TermsOfService, User,
} from '@database/models/server';
import {serverSchema} from '@database/schema/server';
import {getActiveServer, getServer} from '@queries/app/servers';
import {queryActiveServer, queryServer} from '@queries/app/servers';
import {deleteIOSDatabase, getIOSAppGroupDetails} from '@utils/mattermost_managed';
import {hashCode} from '@utils/security';
@ -114,7 +114,7 @@ class DatabaseManager {
if (serverUrl) {
try {
const databaseName = hashCode(dbName);
const databaseName = hashCode(serverUrl);
const databaseFilePath = this.getDatabaseFilePath(databaseName);
const migrations = ServerDatabaseMigrations;
const modelClasses = this.serverModels;
@ -202,7 +202,7 @@ class DatabaseManager {
*/
private isServerPresent = async (serverUrl: string): Promise<boolean> => {
if (this.appDatabase?.database) {
const server = await getServer(this.appDatabase.database, serverUrl);
const server = await queryServer(this.appDatabase.database, serverUrl);
return Boolean(server);
}
@ -216,7 +216,7 @@ class DatabaseManager {
public getActiveServerUrl = async (): Promise<string|null|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
const server = await queryActiveServer(database);
return server?.url;
}
@ -230,7 +230,7 @@ class DatabaseManager {
public getActiveServerDatabase = async (): Promise<Database|undefined> => {
const database = this.appDatabase?.database;
if (database) {
const server = await getActiveServer(database);
const server = await queryActiveServer(database);
if (server?.url) {
return this.serverDatabases[server.url].database;
}
@ -268,10 +268,10 @@ class DatabaseManager {
public deleteServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
const server = await queryServer(database, serverUrl);
if (server) {
database.action(() => {
server.update((record) => {
database.action(async () => {
await server.update((record) => {
record.lastActiveAt = 0;
});
});
@ -291,7 +291,7 @@ class DatabaseManager {
public destroyServerDatabase = async (serverUrl: string): Promise<void> => {
if (this.appDatabase?.database) {
const database = this.appDatabase?.database;
const server = await getServer(database, serverUrl);
const server = await queryServer(database, serverUrl);
if (server) {
database.action(async () => {
await server.destroyPermanently();

View file

@ -5,6 +5,7 @@ import {Model} from '@nozbe/watermelondb';
import {json} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import {safeParseJSON} from '@utils/helpers';
const {GLOBAL} = MM_TABLES.APP;
@ -19,5 +20,5 @@ export default class Global extends Model {
static table = GLOBAL;
/** value : The value part of the key-value combination and whose key will be the id column */
@json('value', (rawJson) => rawJson) value!: any;
@json('value', safeParseJSON) value!: any;
}

View file

@ -5,6 +5,7 @@ import Model, {Associations} from '@nozbe/watermelondb/Model';
import {field, json} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import {safeParseJSON} from '@utils/helpers';
const {CHANNEL, DRAFT, POST} = MM_TABLES.SERVER;
@ -35,5 +36,5 @@ export default class Draft extends Model {
@field('root_id') rootId!: string;
/** files : The files field will hold an array of file objects that have not yet been uploaded and persisted within the FILE table */
@json('files', (rawJson) => rawJson) files!: FileInfo[];
@json('files', safeParseJSON) files!: FileInfo[];
}

View file

@ -5,8 +5,10 @@ import {Relation} from '@nozbe/watermelondb';
import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import Channel from '@typings/database/models/servers/channel';
import {MM_TABLES} from '@constants/database';
import {safeParseJSON} from '@utils/helpers';
import type Channel from '@typings/database/models/servers/channel';
const {CHANNEL, MY_CHANNEL_SETTINGS} = MM_TABLES.SERVER;
@ -29,7 +31,7 @@ export default class MyChannelSettings extends Model {
@field('channel_id') channelId!: string;
/** notify_props : Configurations with regards to this channel */
@json('notify_props', (rawJson) => rawJson) notifyProps!: NotifyProps;
@json('notify_props', safeParseJSON) notifyProps!: NotifyProps;
/** channel : The relation pointing to the CHANNEL table */
@immutableRelation(CHANNEL, 'channel_id') channel!: Relation<Channel>;

View file

@ -6,13 +6,15 @@ import {children, field, immutableRelation, json, lazy} from '@nozbe/watermelond
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/models/servers/channel';
import Draft from '@typings/database/models/servers/draft';
import File from '@typings/database/models/servers/file';
import PostInThread from '@typings/database/models/servers/posts_in_thread';
import PostMetadata from '@typings/database/models/servers/post_metadata';
import Reaction from '@typings/database/models/servers/reaction';
import User from '@typings/database/models/servers/user';
import {safeParseJSON} from '@utils/helpers';
import type Channel from '@typings/database/models/servers/channel';
import type Draft from '@typings/database/models/servers/draft';
import type File from '@typings/database/models/servers/file';
import type PostInThread from '@typings/database/models/servers/posts_in_thread';
import type PostMetadata from '@typings/database/models/servers/post_metadata';
import type Reaction from '@typings/database/models/servers/reaction';
import type User from '@typings/database/models/servers/user';
const {CHANNEL, DRAFT, FILE, POST, POSTS_IN_THREAD, POST_METADATA, REACTION, USER} = MM_TABLES.SERVER;
@ -88,7 +90,7 @@ export default class Post extends Model {
@field('user_id') userId!: string;
/** props : Additional attributes for this props */
@json('props', (rawJson) => rawJson) props!: object;
@json('props', safeParseJSON) props!: object;
// A draft can be associated with this post for as long as this post is a parent post
@lazy draft = this.collections.get(DRAFT).query(Q.on(POST, 'id', this.id)) as Query<Draft>;

View file

@ -6,8 +6,10 @@ import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import {PostMetadataData, PostMetadataType} from '@typings/database/database';
import Post from '@typings/database/models/servers/post';
import {safeParseJSON} from '@utils/helpers';
import type {PostMetadataData, PostMetadataType} from '@typings/database/database';
import type Post from '@typings/database/models/servers/post';
const {POST, POST_METADATA} = MM_TABLES.SERVER;
@ -32,7 +34,7 @@ export default class PostMetadata extends Model {
@field('type') type!: PostMetadataType;
/** data : Different types of data ranging from embeds to images. */
@json('data', (rawJson) => rawJson) data!: PostMetadataData;
@json('data', safeParseJSON) data!: PostMetadataData;
/** post: The record representing the POST parent. */
@immutableRelation(POST, 'post_id') post!: Relation<Post>;

View file

@ -5,6 +5,7 @@ import {Model} from '@nozbe/watermelondb';
import {field, json} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import {safeParseJSON} from '@utils/helpers';
const {ROLE} = MM_TABLES.SERVER;
@ -17,6 +18,6 @@ export default class Role extends Model {
@field('name') name!: string;
/** permissions : The different permissions associated to that role */
@json('permissions', (rawJson) => rawJson) permissions!: string[];
@json('permissions', safeParseJSON) permissions!: string[];
}

View file

@ -5,6 +5,7 @@ import {Model} from '@nozbe/watermelondb';
import {json} from '@nozbe/watermelondb/decorators';
import {MM_TABLES} from '@constants/database';
import {safeParseJSON} from '@utils/helpers';
const {SYSTEM} = MM_TABLES.SERVER;
@ -18,5 +19,5 @@ export default class System extends Model {
static table = SYSTEM;
/** value : The value for that config/information and whose key will be the id column */
@json('value', (rawJson) => rawJson) value!: any;
@json('value', safeParseJSON) value!: any;
}

View file

@ -6,7 +6,9 @@ import {field, immutableRelation, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Team from '@typings/database/models/servers/team';
import {safeParseJSON} from '@utils/helpers';
import type Team from '@typings/database/models/servers/team';
const {TEAM, TEAM_CHANNEL_HISTORY} = MM_TABLES.SERVER;
@ -29,7 +31,7 @@ export default class TeamChannelHistory extends Model {
@field('team_id') teamId!: string;
/** channel_ids : An array containing the last 5 channels visited within this team order by recency */
@json('channel_ids', (rawJson) => rawJson) channelIds!: string[];
@json('channel_ids', safeParseJSON) channelIds!: string[];
/** team : The related record from the parent Team model */
@immutableRelation(TEAM, 'team_id') team!: Relation<Team>;

View file

@ -5,13 +5,15 @@ import {children, field, json} from '@nozbe/watermelondb/decorators';
import Model, {Associations} from '@nozbe/watermelondb/Model';
import {MM_TABLES} from '@constants/database';
import Channel from '@typings/database/models/servers/channel';
import ChannelMembership from '@typings/database/models/servers/channel_membership';
import GroupMembership from '@typings/database/models/servers/group_membership';
import Post from '@typings/database/models/servers/post';
import Preference from '@typings/database/models/servers/preference';
import Reaction from '@typings/database/models/servers/reaction';
import TeamMembership from '@typings/database/models/servers/team_membership';
import {safeParseJSON} from '@utils/helpers';
import type Channel from '@typings/database/models/servers/channel';
import type ChannelMembership from '@typings/database/models/servers/channel_membership';
import type GroupMembership from '@typings/database/models/servers/group_membership';
import type Post from '@typings/database/models/servers/post';
import type Preference from '@typings/database/models/servers/preference';
import type Reaction from '@typings/database/models/servers/reaction';
import type TeamMembership from '@typings/database/models/servers/team_membership';
const {
CHANNEL,
@ -103,15 +105,15 @@ export default class User extends Model {
@field('username') username!: string;
/** notify_props : Notification preferences/configurations */
@json('notify_props', (rawJson) => rawJson) notifyProps!: NotifyProps;
@json('notify_props', safeParseJSON) notifyProps!: NotifyProps;
/** props : Custom objects ( e.g. custom status) can be stored in there. Its type definition is known as
* 'excess property check' in Typescript land. We keep using it till we build up the final shape of this object.
*/
@json('props', (rawJson) => rawJson) props!: UserProps;
@json('props', safeParseJSON) props!: UserProps;
/** timezone : The timezone for this user */
@json('timezone', (rawJson) => rawJson) timezone!: Timezone;
@json('timezone', safeParseJSON) timezone!: Timezone;
/** channelsCreated : All the channels that this user created */
@children(CHANNEL) channelsCreated!: Channel[];

View file

@ -205,8 +205,8 @@ export function getLocaleFromLanguage(lang: string) {
return locale;
}
export function resetMomentLocale() {
moment.locale(DEFAULT_LOCALE);
export function resetMomentLocale(locale?: string) {
moment.locale(locale || DEFAULT_LOCALE);
}
export function getTranslations(locale?: string) {

View file

@ -8,7 +8,6 @@ import * as KeyChain from 'react-native-keychain';
import DatabaseManager from '@database/manager';
import * as analytics from '@init/analytics';
import {getIOSAppGroupDetails} from '@utils/mattermost_managed';
import {getCSRFFromCookie} from '@utils/security';
import type {ServerCredential} from '@typings/credentials';
@ -55,8 +54,8 @@ export const getActiveServerUrl = async () => {
return serverUrl || undefined;
};
export const setServerCredentials = (serverUrl: string, userId: string, token: string) => {
if (!(serverUrl && userId && token)) {
export const setServerCredentials = (serverUrl: string, token: string) => {
if (!(serverUrl && token)) {
return;
}
@ -71,24 +70,21 @@ export const setServerCredentials = (serverUrl: string, userId: string, token: s
accessGroup,
securityLevel: KeyChain.SECURITY_LEVEL.SECURE_SOFTWARE,
};
KeyChain.setInternetCredentials(serverUrl, userId, token, options);
KeyChain.setInternetCredentials(serverUrl, token, token, options);
} catch (e) {
console.warn('could not set credentials', e); //eslint-disable-line no-console
}
};
export const removeServerCredentials = async (serverUrl: string) => {
// TODO: invalidate client and remove tokens
KeyChain.resetInternetCredentials(serverUrl);
AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
await KeyChain.resetInternetCredentials(serverUrl);
await AsyncStorage.removeItem(ASYNC_STORAGE_CURRENT_SERVER_KEY);
};
export const removeActiveServerCredentials = async () => {
const serverUrl = await getActiveServerUrl();
if (serverUrl) {
removeServerCredentials(serverUrl);
await removeServerCredentials(serverUrl);
}
};
@ -109,12 +105,6 @@ export const getServerCredentials = async (serverUrl: string): Promise<ServerCre
const analyticsClient = analytics.get(serverUrl);
analyticsClient?.setUserId(userId);
const csrf = await getCSRFFromCookie(serverUrl);
// eslint-disable-next-line no-console
console.log('CSRF', csrf);
// TODO: Create client and set token / CSRF
return {serverUrl, userId, token};
}
}

View file

@ -1,145 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DeviceInfo from 'react-native-device-info';
import RNFetchBlob from 'rn-fetch-blob';
import urlParse from 'url-parse';
import LocalConfig from '@assets/config';
import {Client4} from '@client/rest';
import {HEADER_TOKEN, HEADER_X_CLUSTER_ID, HEADER_X_VERSION_ID} from '@client/rest/constants';
import ClientError from '@client/rest/error';
// import EventEmitter from '@mm-redux/utils/event_emitter';
// import {General} from '@mm-redux/constants';
import {t} from '@utils/i18n';
/* eslint-disable no-throw-literal */
const DEFAULT_TIMEOUT = 10000;
const handleRedirectProtocol = (url, response) => {
const serverUrl = Client4.getUrl();
const parsed = urlParse(url);
const {redirects} = response.rnfbRespInfo;
if (redirects) {
const redirectUrl = urlParse(redirects[redirects.length - 1]);
if (serverUrl === parsed.origin && parsed.host === redirectUrl.host && parsed.protocol !== redirectUrl.protocol) {
Client4.setUrl(serverUrl.replace(parsed.protocol, redirectUrl.protocol));
}
}
};
Client4.doFetchWithResponse = async (url, options) => {
// eslint-disable-next-line no-console
console.log('Request endpoint', url);
const customHeaders = LocalConfig.CustomRequestHeaders;
let requestOptions = {
...Client4.getOptions(options),
};
if (customHeaders && Object.keys(customHeaders).length > 0) {
requestOptions = {
...requestOptions,
headers: {
...requestOptions.headers,
...LocalConfig.CustomRequestHeaders,
},
};
}
let response;
let headers;
let data;
try {
response = await fetch(url, requestOptions);
headers = response.headers;
if (!url.startsWith('https') && response.rnfbRespInfo && response.rnfbRespInfo.redirects && response.rnfbRespInfo.redirects.length > 1) {
handleRedirectProtocol(url, response);
}
data = await response.json();
} catch (err) {
if (response && response.resp && response.resp.data && response.resp.data.includes('SSL certificate')) {
throw new ClientError(Client4.getUrl(), {
message: 'You need to use a valid client certificate in order to connect to this Mattermost server',
status_code: 401,
url,
details: err,
});
}
throw new ClientError(Client4.getUrl(), {
message: 'Received invalid response from the server.',
intl: {
id: t('mobile.request.invalid_response'),
defaultMessage: 'Received invalid response from the server.',
},
url,
details: err,
});
}
const clusterId = headers[HEADER_X_CLUSTER_ID] || headers[HEADER_X_CLUSTER_ID.toLowerCase()];
if (clusterId && Client4.clusterId !== clusterId) {
Client4.clusterId = clusterId; /* eslint-disable-line require-atomic-updates */
}
const token = headers[HEADER_TOKEN] || headers[HEADER_TOKEN.toLowerCase()];
if (token) {
Client4.setToken(token);
}
const serverVersion = headers[HEADER_X_VERSION_ID] || headers[HEADER_X_VERSION_ID.toLowerCase()];
if (serverVersion && !headers['Cache-Control'] && Client4.serverVersion !== serverVersion) {
Client4.serverVersion = serverVersion; /* eslint-disable-line require-atomic-updates */
// EventEmitter.emit(General.SERVER_VERSION_CHANGED, serverVersion);
}
if (response.ok) {
const headersMap = new Map();
Object.keys(headers).forEach((key) => {
headersMap.set(key, headers[key]);
});
return {
response,
headers: headersMap,
data,
};
}
const msg = data.message || '';
if (Client4.logToConsole) {
console.error(msg); // eslint-disable-line no-console
}
throw new ClientError(Client4.getUrl(), {
message: msg,
server_error_id: data.id,
status_code: data.status_code,
url,
});
};
const initFetchConfig = async () => {
const fetchConfig = {
auto: true,
timeout: DEFAULT_TIMEOUT, // Set the base timeout for every request to 5s
};
const userAgent = await DeviceInfo.getUserAgent();
Client4.setUserAgent(userAgent);
window.fetch = new RNFetchBlob.polyfill.Fetch(fetchConfig).build();
return true;
};
initFetchConfig();
export default initFetchConfig;

View file

@ -2,17 +2,20 @@
// See LICENSE.txt for license information.
import {Alert, DeviceEventEmitter, Linking, Platform} from 'react-native';
import CookieManager, {Cookie} from '@react-native-community/cookies';
import {FileSystem} from 'react-native-unimodules';
import CookieManager, {Cookie} from '@react-native-cookies/cookies';
import semver from 'semver';
import {fetchConfigAndLicense} from '@actions/remote/general';
import LocalConfig from '@assets/config.json';
import {Navigation} from '@constants';
import {General, REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV} from '@constants';
import DatabaseManager from '@database/manager';
import {DEFAULT_LOCALE, getTranslations, resetMomentLocale, t} from '@i18n';
import * as analytics from '@init/analytics';
import {getServerCredentials, removeServerCredentials} from '@init/credentials';
import {getLaunchPropsFromDeepLink, relaunchApp} from '@init/launch';
import NetworkManager from '@init/network_manager';
import PushNotifications from '@init/push_notifications';
import {queryCurrentUser} from '@queries/servers/user';
import {LaunchType} from '@typings/launch';
import {deleteFileCache} from '@utils/file';
@ -22,10 +25,9 @@ class GlobalEventHandler {
JavascriptAndNativeErrorHandler: jsAndNativeErrorHandler | undefined;
constructor() {
DeviceEventEmitter.addListener(Navigation.NAVIGATION_RESET, this.onLogout);
// DeviceEventEmitter.addListener(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
// DeviceEventEmitter.addListener(General.CONFIG_CHANGED, this.onServerConfigChanged);
DeviceEventEmitter.addListener(General.SERVER_LOGOUT, this.onLogout);
DeviceEventEmitter.addListener(General.SERVER_VERSION_CHANGED, this.onServerVersionChanged);
DeviceEventEmitter.addListener(General.CONFIG_CHANGED, this.onServerConfigChanged);
Linking.addEventListener('url', this.onDeepLink);
}
@ -47,74 +49,44 @@ class GlobalEventHandler {
};
onDeepLink = (event: LinkingCallbackArg) => {
if (event.url?.startsWith(REDIRECT_URL_SCHEME) || event.url?.startsWith(REDIRECT_URL_SCHEME_DEV)) {
return;
}
if (event.url) {
const props = getLaunchPropsFromDeepLink(event.url);
relaunchApp(props);
}
};
clearCookies = async (serverUrl: string | undefined, webKit: boolean) => {
clearCookies = async (serverUrl: string, webKit: boolean) => {
try {
if (serverUrl) {
const cookies = await CookieManager.get(serverUrl, webKit);
const values = Object.values(cookies);
values.forEach((cookie: Cookie) => {
CookieManager.clearByName(serverUrl, cookie.name, webKit);
});
} else {
await CookieManager.clearAll(webKit);
}
const cookies = await CookieManager.get(serverUrl, webKit);
const values = Object.values(cookies);
values.forEach((cookie: Cookie) => {
CookieManager.clearByName(serverUrl, cookie.name, webKit);
});
} catch (error) {
// Nothing to clear
}
}
clearCookiesAndWebData = async (serverUrl?: string) => {
clearCookiesForServer = async (serverUrl: string) => {
this.clearCookies(serverUrl, false);
if (Platform.OS === 'ios') {
// Also delete any cookies that were set by react-native-webview
this.clearCookies(serverUrl, true);
}
// TODO: Only execute this if there are no more servers
switch (Platform.OS) {
case 'ios': {
const mainPath = FileSystem.documentDirectory?.split('/').slice(0, -1).join('/');
const libraryDir = `${mainPath}/Library`;
const cookiesDir = `${libraryDir}/Cookies`;
const cookies = await FileSystem.getInfoAsync(cookiesDir);
const webkitDir = `${libraryDir}/WebKit`;
const webkit = await FileSystem.getInfoAsync(webkitDir);
if (cookies.exists) {
FileSystem.deleteAsync(cookiesDir);
}
if (webkit.exists) {
FileSystem.deleteAsync(webkitDir);
}
break;
}
case 'android': {
const cacheDir = FileSystem.cacheDirectory;
const mainPath = cacheDir?.split('/').slice(0, -1).join('/');
const cookies = await FileSystem.getInfoAsync(`${mainPath}/app_webview/Cookies`);
const cookiesJ = await FileSystem.getInfoAsync(`${mainPath}/app_webview/Cookies-journal`);
if (cookies.exists) {
FileSystem.deleteAsync(`${mainPath}/app_webview/Cookies`);
}
if (cookiesJ.exists) {
FileSystem.deleteAsync(`${mainPath}/app_webview/Cookies-journal`);
}
break;
}
} else if (Platform.OS === 'android') {
CookieManager.flush();
}
};
onLogout = async (serverUrl: string) => {
// TODO: Close and invalidate ApiClient & WebSocket client
await removeServerCredentials(serverUrl);
// TODO WebSocket: invalidate WebSocket client
NetworkManager.invalidateClient(serverUrl);
await DatabaseManager.deleteServerDatabase(serverUrl);
const analyticsClient = analytics.get(serverUrl);
if (analyticsClient) {
@ -122,37 +94,28 @@ class GlobalEventHandler {
analytics.invalidate(serverUrl);
}
removeServerCredentials(serverUrl);
deleteFileCache(serverUrl);
PushNotifications.clearNotifications(serverUrl);
// TODO: remove files for the server
deleteFileCache();
PushNotifications.clearNotifications();
// TODO: Only execute this if there are no more servers
// in case there are other servers switch to the appropriate locale
resetMomentLocale();
await this.clearCookiesAndWebData();
relaunchApp({launchType: LaunchType.Normal});
this.resetLocale();
this.clearCookiesForServer(serverUrl);
relaunchApp({launchType: LaunchType.Normal}, true);
};
onServerConfigChanged = (serverUrl: string, config: ClientConfig) => {
onServerConfigChanged = ({serverUrl, config}: {serverUrl: string, config: ClientConfig}) => {
this.configureAnalytics(serverUrl, config);
// TODO: Add minimum server version checked with cloud support
// if (isMinimumServerVersion(Client4.serverVersion, 5, 24) && config.ExtendSessionLengthWithActivity === 'true') {
// PushNotifications.cancelAllLocalNotifications();
// }
if (config.ExtendSessionLengthWithActivity === 'true') {
PushNotifications.cancelAllLocalNotifications();
}
};
onServerVersionChanged = async (serverUrl: string, serverVersion?: string) => {
onServerVersionChanged = async ({serverUrl, serverVersion}: {serverUrl: string, serverVersion?: string}) => {
const match = serverVersion?.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g);
const version = match && match[0];
const locale = DEFAULT_LOCALE;
const translations = getTranslations(locale);
// TODO: Handle when the server is cloud
if (version) {
if (semver.valid(version) && semver.lt(version, LocalConfig.MinServerVersion)) {
Alert.alert(
@ -166,11 +129,20 @@ class GlobalEventHandler {
);
}
// TODO: Set server version in client and
// reload config and license unless done somewhere else
fetchConfigAndLicense(serverUrl);
}
};
resetLocale = async () => {
if (Object.keys(DatabaseManager.serverDatabases).length) {
const serverDatabase = await DatabaseManager.getActiveServerDatabase();
const user = await queryCurrentUser(serverDatabase!);
resetMomentLocale(user?.locale);
} else {
resetMomentLocale();
}
}
serverUpgradeNeeded = async (serverUrl: string) => {
const credentials = await getServerCredentials(serverUrl);

View file

@ -18,7 +18,7 @@ export const initialLaunch = async () => {
}
const notification = await Notifications.getInitialNotification();
if (notification) {
if (notification && notification.payload?.type === 'message') {
launchAppFromNotification(notification);
return;
}
@ -56,7 +56,6 @@ const launchApp = async (props: LaunchProps, resetNavigation = true) => {
if (serverUrl) {
const credentials = await getServerCredentials(serverUrl);
if (credentials) {
launchToChannel({...props, serverUrl}, resetNavigation);
return;
@ -95,8 +94,8 @@ const launchToServer = (props: LaunchProps, resetNavigation: Boolean) => {
goToScreen(Screens.SERVER, title, {...props});
};
export const relaunchApp = (props: LaunchProps) => {
launchApp(props, false);
export const relaunchApp = (props: LaunchProps, resetNavigation = false) => {
launchApp(props, resetNavigation);
};
export const getLaunchPropsFromDeepLink = (deepLinkUrl: string): LaunchProps => {

123
app/init/network_manager.ts Normal file
View file

@ -0,0 +1,123 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {DeviceEventEmitter} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import Emm from '@mattermost/react-native-emm';
import {
APIClientErrorEvent,
APIClientErrorEventHandler,
getOrCreateAPIClient,
RetryTypes,
} from '@mattermost/react-native-network-client';
import LocalConfig from '@assets/config.json';
import {Client} from '@client/rest';
import * as ClientConstants from '@client/rest/constants';
import {CERTIFICATE_ERRORS} from '@constants/network';
import ManagedApp from '@init/managed_app';
import {getCSRFFromCookie} from '@utils/security';
import type {ServerCredential} from '@typings/credentials';
const CLIENT_CERTIFICATE_IMPORT_ERROR_CODES = [-103, -104, -105, -108];
const CLIENT_CERTIFICATE_MISSING_ERROR_CODE = -200;
class NetworkManager {
private clients: Record<string, Client> = {};
private DEFAULT_CONFIG = {
headers: {
'X-Requested-With': 'XMLHttpRequest',
...LocalConfig.CustomRequestHeaders,
},
sessionConfiguration: {
allowsCellularAccess: true,
waitsForConnectivity: false,
timeoutIntervalForRequest: 30000,
timeoutIntervalForResource: 30000,
httpMaximumConnectionsPerHost: 10,
cancelRequestsOnUnauthorized: true,
},
retryPolicyConfiguration: {
type: RetryTypes.EXPONENTIAL_RETRY,
retryLimit: 2,
exponentialBackoffBase: 2,
exponentialBackoffScale: 0.5,
},
requestAdapterConfiguration: {
bearerAuthTokenResponseHeader: 'token',
},
};
public init = async (serverCredentials: ServerCredential[]) => {
for await (const {serverUrl, token} of serverCredentials) {
try {
this.createClient(serverUrl, token);
} catch (error) {
console.log(error); //eslint-disable-line no-console
}
}
}
public invalidateClient = (serverUrl: string) => {
this.clients[serverUrl]?.invalidate();
delete this.clients[serverUrl];
}
public getClient = (serverUrl: string) => {
const client = this.clients[serverUrl];
if (!client) {
throw new Error(`${serverUrl} client not found`);
}
return client;
}
public createClient = async (serverUrl: string, bearerToken?: string) => {
const config = await this.buildConfig();
const {client} = await getOrCreateAPIClient(serverUrl, config, this.clientErrorEventHandler);
const csrfToken = await getCSRFFromCookie(serverUrl);
this.clients[serverUrl] = new Client(client, serverUrl, bearerToken, csrfToken);
return this.clients[serverUrl];
}
private buildConfig = async () => {
const userAgent = await DeviceInfo.getUserAgent();
const headers: Record<string, string> = {
...this.DEFAULT_CONFIG.headers,
[ClientConstants.HEADER_USER_AGENT]: userAgent,
};
const config = {
...this.DEFAULT_CONFIG,
headers,
};
if (ManagedApp.enabled) {
const managedConfig = await Emm.getManagedConfig();
if (managedConfig?.useVPN === 'true') {
config.sessionConfiguration.waitsForConnectivity = true;
}
if (managedConfig?.timeoutVPN) {
config.sessionConfiguration.timeoutIntervalForResource = parseInt(managedConfig.timeoutVPN, 10);
}
}
return config;
}
private clientErrorEventHandler: APIClientErrorEventHandler = (event: APIClientErrorEvent) => {
if (CLIENT_CERTIFICATE_IMPORT_ERROR_CODES.includes(event.errorCode)) {
DeviceEventEmitter.emit(CERTIFICATE_ERRORS.CLIENT_CERTIFICATE_IMPORT_ERROR, event.serverUrl);
} else if (CLIENT_CERTIFICATE_MISSING_ERROR_CODE === event.errorCode) {
DeviceEventEmitter.emit(CERTIFICATE_ERRORS.CLIENT_CERTIFICATE_MISSING, event.serverUrl);
}
};
}
export default new NetworkManager();

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import {DEFAULT_LOCALE, getLocalizedMessage, t} from '@i18n';
import {AppState, AppStateStatus, DeviceEventEmitter, EmitterSubscription, Platform} from 'react-native';
import {AppState, DeviceEventEmitter, Platform} from 'react-native';
import DeviceInfo from 'react-native-device-info';
import {
Notification,
@ -15,10 +15,13 @@ import {
Registered,
} from 'react-native-notifications';
import {Device, Navigation, View} from '@constants';
import {Device, General, Navigation} from '@constants';
import {GLOBAL_IDENTIFIERS} from '@constants/database';
import DatabaseManager from '@database/manager';
import {getLaunchPropsFromNotification, relaunchApp} from '@init/launch';
import NativeNotifications from '@notifications';
import {queryMentionCount} from '@queries/app/global';
import {queryCurrentChannelId} from '@queries/servers/system';
import {showOverlay} from '@screens/navigation';
const CATEGORY = 'CAN_REPLY';
@ -29,30 +32,32 @@ const NOTIFICATION_TYPE = {
SESSION: 'session',
};
//todo: Do we need Ephemeral store? Should we refactor this file ?
class PushNotifications {
configured = false;
pushNotificationListener: EmitterSubscription | undefined;
constructor() {
init() {
Notifications.registerRemoteNotifications();
Notifications.events().registerNotificationOpened(this.onNotificationOpened);
Notifications.events().registerRemoteNotificationsRegistered(this.onRemoteNotificationsRegistered);
Notifications.events().registerNotificationReceivedBackground(this.onNotificationReceivedBackground);
Notifications.events().registerNotificationReceivedForeground(this.onNotificationReceivedForeground);
AppState.addEventListener('change', this.onAppStateChange);
}
cancelAllLocalNotifications = () => {
Notifications.cancelAllLocalNotifications();
};
clearNotifications = () => {
this.setApplicationIconBadgeNumber(0);
clearNotifications = (serverUrl: string) => {
// TODO Notifications: Only cancel the local notifications that belong to this server
// TODO: Only cancel the local notifications that belong to this server
// eslint-disable-next-line no-console
console.log('Clear notifications for server', serverUrl);
this.cancelAllLocalNotifications();
if (Platform.OS === 'ios') {
// TODO Notifications: Set the badge number to the total amount of mentions on other servers
Notifications.ios.setBadgeCount(0);
}
};
clearChannelNotifications = async (channelId: string) => {
@ -69,13 +74,21 @@ class PushNotifications {
}
} else {
const ids: string[] = [];
const badgeCount = notifications.length;
// TODO: Set the badgeCount with default database mention count aggregate
let badgeCount = notifications.length;
for (const notification of notifications) {
if (notification.channel_id === channelId) {
ids.push(notification.identifier);
badgeCount--;
}
}
// Set the badgeCount with default database mention count aggregate
const appDatabase = DatabaseManager.appDatabase?.database;
if (appDatabase) {
const mentions = await queryMentionCount(appDatabase);
if (mentions) {
badgeCount = parseInt(mentions, 10);
}
}
@ -83,16 +96,17 @@ class PushNotifications {
NativeNotifications.removeDeliveredNotifications(ids);
}
this.setApplicationIconBadgeNumber(badgeCount);
if (Platform.OS === 'ios') {
badgeCount = badgeCount <= 0 ? 0 : badgeCount;
Notifications.ios.setBadgeCount(badgeCount);
}
}
};
createReplyCategory = () => {
const locale = DEFAULT_LOCALE; // TODO: Get the current user locale to replace the old getCurrentLocale(state);
const replyTitle = getLocalizedMessage(locale, t('mobile.push_notification_reply.title'));
const replyButton = getLocalizedMessage(locale, t('mobile.push_notification_reply.button'));
const replyPlaceholder = getLocalizedMessage(locale, t('mobile.push_notification_reply.placeholder'));
const replyTitle = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.title'));
const replyButton = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.button'));
const replyPlaceholder = getLocalizedMessage(DEFAULT_LOCALE, t('mobile.push_notification_reply.placeholder'));
const replyTextInput: NotificationTextInput = {buttonTitle: replyButton, placeholder: replyPlaceholder};
const replyAction = new NotificationAction(REPLY_ACTION, 'background', replyTitle, true, replyTextInput);
return new NotificationCategory(CATEGORY, [replyAction]);
@ -104,13 +118,13 @@ class PushNotifications {
if (payload) {
switch (payload.type) {
case NOTIFICATION_TYPE.CLEAR:
// Mark the channel as read
// TODO Notifications: Mark the channel as read
break;
case NOTIFICATION_TYPE.MESSAGE:
// fetch the posts for the channel
// TODO Notifications: fetch the posts for the channel
if (foreground) {
// Show the in-app notification
this.handleInAppNotification(notification);
} else if (userInteraction && !payload.userInfo?.local) {
const props = getLaunchPropsFromNotification(notification);
relaunchApp(props);
@ -120,8 +134,9 @@ class PushNotifications {
// eslint-disable-next-line no-console
console.log('Session expired notification');
// Logout the user from the server that matches the notification
if (payload.server_url) {
DeviceEventEmitter.emit(General.SERVER_LOGOUT, payload.server_url);
}
break;
}
}
@ -130,17 +145,20 @@ class PushNotifications {
handleInAppNotification = async (notification: NotificationWithData) => {
const {payload} = notification;
// TODO: Get current channel from the database
const currentChannelId = '';
if (payload?.server_url) {
const database = DatabaseManager.serverDatabases[payload.server_url]?.database;
const currentChannelId = await queryCurrentChannelId(database);
const channelId = currentChannelId?.value;
if (payload?.channel_id !== currentChannelId) {
const screen = 'Notification';
const passProps = {
notification,
};
if (channelId && payload.channel_id !== channelId) {
const screen = 'Notification';
const passProps = {
notification,
};
DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY);
showOverlay(screen, passProps);
DeviceEventEmitter.emit(Navigation.NAVIGATION_SHOW_OVERLAY);
showOverlay(screen, passProps);
}
}
};
@ -148,23 +166,6 @@ class PushNotifications {
Notifications.postLocalNotification(notification);
};
onAppStateChange = (appState: AppStateStatus) => {
const isActive = appState === 'active';
const isBackground = appState === 'background';
if (isActive) {
if (!this.pushNotificationListener) {
this.pushNotificationListener = DeviceEventEmitter.addListener(
View.NOTIFICATION_IN_APP,
this.handleInAppNotification,
);
}
} else if (isBackground) {
this.pushNotificationListener?.remove();
this.pushNotificationListener = undefined;
}
};
onNotificationOpened = (notification: NotificationWithData, completion: () => void) => {
notification.userInteraction = true;
this.handleNotification(notification);
@ -204,7 +205,7 @@ class PushNotifications {
}
operator.handleGlobal({
global: [{id: 'deviceToken', value: `${prefix}:${deviceToken}`}],
global: [{id: GLOBAL_IDENTIFIERS.DEVICE_TOKEN, value: `${prefix}:${deviceToken}`}],
prepareRecordsOnly: false,
});
@ -221,13 +222,6 @@ class PushNotifications {
}
};
setApplicationIconBadgeNumber = (value: number) => {
if (Platform.OS === 'ios') {
const count = value < 0 ? 0 : value;
Notifications.ios.setBadgeCount(count);
}
};
scheduleNotification = (notification: Notification) => {
if (notification.fireDate) {
if (Platform.OS === 'ios') {

View file

@ -1,14 +1,27 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MM_TABLES} from '@constants/database';
import {Database, Q} from '@nozbe/watermelondb';
import {GLOBAL_IDENTIFIERS, MM_TABLES} from '@constants/database';
import {Database} from '@nozbe/watermelondb';
import type Global from '@typings/database/models/app/global';
const {APP: {GLOBAL}} = MM_TABLES;
export const getDeviceToken = async (appDatabase: Database) => {
const tokens = (await appDatabase.collections.get(GLOBAL).query(Q.where('id', 'deviceToken')).fetch()) as Global[];
return tokens?.[0]?.value ?? '';
export const queryDeviceToken = async (appDatabase: Database) => {
try {
const tokens = await appDatabase.get(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN) as Global;
return tokens?.value || '';
} catch {
return '';
}
};
export const queryMentionCount = async (appDatabase: Database) => {
try {
const mentions = await appDatabase.get(GLOBAL).find(GLOBAL_IDENTIFIERS.MENTION_COUNT) as Global;
return mentions?.value || '';
} catch {
return '';
}
};

View file

@ -8,18 +8,18 @@ import type Servers from '@typings/database/models/app/servers';
const {APP: {SERVERS}} = MM_TABLES;
export const getServer = async (appDatabase: Database, serverUrl: string) => {
export const queryServer = async (appDatabase: Database, serverUrl: string) => {
const servers = (await appDatabase.collections.get(SERVERS).query(Q.where('url', serverUrl)).fetch()) as Servers[];
return servers?.[0];
};
export const getAllServers = async (appDatabse: Database) => {
export const queryAllServers = async (appDatabse: Database) => {
return (await appDatabse.collections.get(MM_TABLES.APP.SERVERS).query().fetch()) as Servers[];
};
export const getActiveServer = async (appDatabse: Database) => {
export const queryActiveServer = async (appDatabse: Database) => {
try {
const servers = await getAllServers(appDatabse);
const servers = await queryAllServers(appDatabse);
return servers.reduce((a, b) => (b.lastActiveAt > a.lastActiveAt ? b : a));
} catch {
return undefined;

View file

@ -8,7 +8,7 @@ import Role from '@typings/database/models/servers/role';
const {SERVER: {ROLE}} = MM_TABLES;
export const getRoles = async (database: Database) => {
export const queryRoles = async (database: Database) => {
const roles = await database.collections.get(ROLE).query().fetch() as Role[];
return roles;
};

View file

@ -1,36 +1,61 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {Database} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import System from '@typings/database/models/servers/system';
const {SERVER: {SYSTEM}} = MM_TABLES;
export const getCurrentUserId = async (serverDatabase: Database) => {
const currentUserId = await serverDatabase.collections.get(SYSTEM).query(Q.where('id', 'currentUserId')).fetch() as System[];
return currentUserId?.[0];
export const queryCurrentChannelId = async (serverDatabase: Database) => {
try {
const currentChannelId = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID) as System;
return currentChannelId?.value || '';
} catch {
return '';
}
};
export const getCommonSystemValues = async (database: Database) => {
const systemRecords = (await database.collections.get(SYSTEM).query(Q.where('id', Q.oneOf(['config', 'license', 'currentUserId']))).fetch()) as System[];
export const queryCurrentUserId = async (serverDatabase: Database) => {
try {
const currentUserId = await serverDatabase.get(SYSTEM).find(SYSTEM_IDENTIFIERS.CURRENT_USER_ID) as System;
return currentUserId?.value || '';
} catch {
return '';
}
};
export const queryCommonSystemValues = async (database: Database) => {
const systemRecords = (await database.collections.get(SYSTEM).query().fetch()) as System[];
let config = {};
let license = {};
let currentChannelId = '';
let currentTeamId = '';
let currentUserId = '';
systemRecords.forEach((systemRecord) => {
if (systemRecord.id === 'config') {
config = systemRecord.value;
}
if (systemRecord.id === 'license') {
license = systemRecord.value;
}
if (systemRecord.id === 'currentUserId') {
currentUserId = systemRecord.value;
switch (systemRecord.id) {
case SYSTEM_IDENTIFIERS.CONFIG:
config = systemRecord.value;
break;
case SYSTEM_IDENTIFIERS.CURRENT_CHANNEL_ID:
currentChannelId = systemRecord.value;
break;
case SYSTEM_IDENTIFIERS.CURRENT_TEAM_ID:
currentTeamId = systemRecord.value;
break;
case SYSTEM_IDENTIFIERS.CURRENT_USER_ID:
currentUserId = systemRecord.value;
break;
case SYSTEM_IDENTIFIERS.LICENSE:
license = systemRecord.value;
break;
}
});
return {
currentChannelId,
currentTeamId,
currentUserId,
config,
license,

View file

@ -1,12 +1,28 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Database, Q} from '@nozbe/watermelondb';
import {Database} from '@nozbe/watermelondb';
import {MM_TABLES} from '@constants/database';
import User from '@typings/database/models/servers/user';
export const getUserById = async ({userId, database}: { userId: string, database: Database}) => {
const userRecords = (await database.collections.get(MM_TABLES.SERVER.USER).query(Q.where('id', userId)).fetch()) as User[];
return userRecords?.[0];
import type User from '@typings/database/models/servers/user';
import {queryCurrentUserId} from './system';
export const queryUserById = async ({userId, database}: { userId: string, database: Database}) => {
try {
const userRecord = (await database.collections.get(MM_TABLES.SERVER.USER).find(userId)) as User;
return userRecord;
} catch {
return undefined;
}
};
export const queryCurrentUser = async (database: Database) => {
const currentUserId = await queryCurrentUserId(database);
if (currentUserId) {
return queryUserById({userId: currentUserId, database});
}
return undefined;
};

View file

@ -1,19 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import DatabaseManager from '@database/manager';
export const createSessions = async (serverUrl: string, sessions: any) => {
const database = DatabaseManager.serverDatabases[serverUrl].database;
const operator = DatabaseManager.serverDatabases[serverUrl].operator;
if (!database) {
return {error: `${serverUrl} database not found`};
}
await operator.handleSystem({
systems: [{id: 'sessions', value: sessions}],
prepareRecordsOnly: false,
});
return null;
};

View file

@ -1,46 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
export const doPing = async (serverUrl?: string) => {
let data;
const pingError = {
id: 'mobile.server_ping_failed',
defaultMessage: 'Cannot connect to the server. Please check your server URL and internet connection.',
};
try {
if (serverUrl) {
Client4.setUrl(serverUrl);
}
data = await Client4.ping();
if (data.status !== 'OK') {
// successful ping but not the right return {data}
return {error: {intl: pingError}};
}
} catch (error) {
// Client4Error
if (error.status_code === 401) {
// When the server requires a client certificate to connect.
return {error};
}
return {error: {intl: pingError}};
}
return {data};
};
export const fetchConfigAndLicense = async () => {
try {
const [config, license] = await Promise.all<ClientConfig, ClientLicense>([
Client4.getClientConfigOld(),
Client4.getClientLicenseOld(),
]);
return {config, license};
} catch (error) {
return {error};
}
};

View file

@ -1,22 +0,0 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Client4} from '@client/rest';
import {logError} from '@requests/remote/error';
import {forceLogoutIfNecessary} from '@requests/remote/user';
export const getDataRetentionPolicy = async (serverUrl: string) => {
let data = {};
try {
data = await Client4.getDataRetentionPolicy();
} catch (error) {
forceLogoutIfNecessary(serverUrl, error);
//fixme: do we care for the below line ? It seems that the `error` object is never read ??
// dispatch(batchActions([{type: GeneralTypes.RECEIVED_DATA_RETENTION_POLICY, error,},]));
logError(error);
return {error};
}
return data;
};

View file

@ -13,9 +13,11 @@ import {
import type {LaunchProps} from '@typings/launch';
import {logout} from '@actions/remote/user';
declare const global: {HermesInternal: null | {}};
type ChannelProps = LaunchProps
type ChannelProps = LaunchProps;
const Channel = (props: ChannelProps) => {
// TODO: If we have LaunchProps, ensure we load the correct channel/post/modal.
@ -32,6 +34,10 @@ const Channel = (props: ChannelProps) => {
// }
// }
const doLogout = () => {
logout(props.serverUrl!);
};
return (
<>
<StatusBar barStyle='dark-content'/>
@ -48,7 +54,10 @@ const Channel = (props: ChannelProps) => {
)}
<View style={styles.body}>
<View style={styles.sectionContainer}>
<Text style={styles.sectionTitle}>{'Step One'}</Text>
<Text
onPress={doLogout}
style={styles.sectionTitle}
>{'Step One'}</Text>
<Text style={styles.sectionDescription}>
{'Edit '}<Text style={styles.highlight}>{'screens/channel/index.tsx'}</Text>{' to change this'}
{'screen and then come back to see your edits.'}

View file

@ -5,7 +5,7 @@ import {act, waitFor} from '@testing-library/react-native';
import React from 'react';
import {Preferences} from '@constants';
import * as UserAPICalls from '@requests/remote/user';
import * as UserAPICalls from '@actions/remote/user';
import {renderWithIntl, fireEvent} from '@test/intl-test-helper';
import ForgotPassword from './index';
@ -13,6 +13,7 @@ import ForgotPassword from './index';
describe('ForgotPassword', () => {
const baseProps = {
componentId: 'ForgotPassword',
serverUrl: 'https://community.mattermost.com',
theme: Preferences.THEMES.default,
};

View file

@ -9,15 +9,16 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import ErrorText from '@components/error_text';
import FormattedText from '@components/formatted_text';
import {sendPasswordResetEmail} from '@requests/remote/user';
import {sendPasswordResetEmail} from '@actions/remote/user';
import {isEmail} from '@utils/helpers';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type Props = {
serverUrl: string;
theme: Theme;
}
const ForgotPassword = ({theme}: Props) => {
const ForgotPassword = ({serverUrl, theme}: Props) => {
const [email, setEmail] = useState<string>('');
const [error, setError] = useState<string>('');
const [isPasswordLinkSent, setIsPasswordLinkSent] = useState<boolean>(false);
@ -40,7 +41,7 @@ const ForgotPassword = ({theme}: Props) => {
return;
}
const {data, error: apiError = undefined} = await sendPasswordResetEmail(email);
const {data, error: apiError = undefined} = await sendPasswordResetEmail(serverUrl, email);
if (data) {
setIsPasswordLinkSent(true);

View file

@ -25,24 +25,25 @@ import ErrorText, {ClientErrorWithIntl} from '@components/error_text';
import {FORGOT_PASSWORD, MFA} from '@constants/screens';
import FormattedText from '@components/formatted_text';
import {useManagedConfig} from '@mattermost/react-native-emm';
import {scheduleExpiredNotification} from '@requests/remote/push_notification';
import {login} from '@requests/remote/user';
import {scheduleExpiredNotification} from '@actions/remote/push_notification';
import {login} from '@actions/remote/user';
import {goToScreen, resetToChannel} from '@screens/navigation';
import {t} from '@utils/i18n';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
type LoginProps = {
import type {LaunchProps} from '@typings/launch';
interface LoginProps extends LaunchProps {
componentId: string;
config: ClientConfig;
license: ClientLicense;
serverUrl: string;
theme: Theme;
};
}
export const MFA_EXPECTED_ERRORS = ['mfa.validate_token.authenticate.app_error', 'ent.mfa.validate_token.authenticate.app_error'];
const Login: NavigationFunctionComponent = ({config, license, serverUrl, theme}: LoginProps) => {
const Login: NavigationFunctionComponent = ({config, extra, launchError, launchType, license, serverUrl, theme}: LoginProps) => {
const styles = getStyleSheet(theme);
const loginRef = useRef<TextInput>(null);
@ -129,15 +130,15 @@ const Login: NavigationFunctionComponent = ({config, license, serverUrl, theme}:
});
const signIn = async () => {
const result = await login(serverUrl, {loginId: loginId.toLowerCase(), password, config, license});
const result = await login(serverUrl!, {loginId: loginId.toLowerCase(), password, config, license});
if (checkLoginResponse(result)) {
await goToChannel();
}
};
const goToChannel = async () => {
await scheduleExpiredNotification(serverUrl, intl);
resetToChannel({serverUrl});
await scheduleExpiredNotification(serverUrl!, intl);
resetToChannel({extra, launchError, launchType, serverUrl});
};
const checkLoginResponse = (data: any) => {
@ -203,8 +204,12 @@ const Login: NavigationFunctionComponent = ({config, license, serverUrl, theme}:
const onPressForgotPassword = () => {
const screen = FORGOT_PASSWORD;
const title = intl.formatMessage({id: 'password_form.title', defaultMessage: 'Password Reset'});
const passProps = {
theme,
serverUrl,
};
goToScreen(screen, title, {theme});
goToScreen(screen, title, passProps);
};
const createLoginPlaceholder = () => {

View file

@ -9,7 +9,7 @@ import {waitFor, renderWithIntl, fireEvent} from '@test/intl-test-helper';
import Login from './index';
jest.mock('@requests/remote/user', () => {
jest.mock('@actions/remote/user', () => {
return {
login: () => {
return {
@ -137,7 +137,10 @@ describe('Login', () => {
toHaveBeenCalledWith(
'ForgotPassword',
'Password Reset',
{theme: baseProps.theme},
{
serverUrl: baseProps.serverUrl,
theme: baseProps.theme,
},
);
});
});

View file

@ -13,6 +13,8 @@ import {goToScreen} from '@screens/navigation';
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@app/utils/theme';
import type {LaunchProps} from '@typings/launch';
import EmailOption from './email';
import GitLabOption from './gitlab';
import GoogleOption from './google';
@ -21,7 +23,7 @@ import Office365Option from './office365';
import OpenIdOption from './open_id';
import SamlOption from './saml';
type LoginOptionsProps = {
interface LoginOptionsProps extends LaunchProps {
componentId: string;
serverUrl: string;
config: ClientConfig;
@ -57,7 +59,7 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
},
}));
const LoginOptions: NavigationFunctionComponent = ({config, license, serverUrl, theme}: LoginOptionsProps) => {
const LoginOptions: NavigationFunctionComponent = ({config, extra, launchType, launchError, license, serverUrl, theme}: LoginOptionsProps) => {
const intl = useIntl();
const styles = getStyles(theme);
@ -65,13 +67,13 @@ const LoginOptions: NavigationFunctionComponent = ({config, license, serverUrl,
const screen = LOGIN;
const title = intl.formatMessage({id: 'mobile.routes.login', defaultMessage: 'Login'});
goToScreen(screen, title, {config, license, serverUrl, theme});
goToScreen(screen, title, {config, extra, launchError, launchType, license, serverUrl, theme});
});
const displaySSO = preventDoubleTap((ssoType: string) => {
const screen = SSO;
const title = intl.formatMessage({id: 'mobile.routes.sso', defaultMessage: 'Single Sign-On'});
goToScreen(screen, title, {config, license, theme, ssoType, serverUrl});
goToScreen(screen, title, {config, extra, launchError, launchType, license, theme, ssoType, serverUrl});
});
return (

View file

@ -18,7 +18,7 @@ import {SafeAreaView} from 'react-native-safe-area-context';
import ErrorText from '@components/error_text';
import FormattedText from '@components/formatted_text';
import {login} from '@requests/remote/user';
import {login} from '@actions/remote/user';
import {Config} from '@typings/database/models/servers/config';
import {License} from '@typings/database/models/servers/license';
import {t} from '@utils/i18n';
@ -148,7 +148,7 @@ const MFA = ({config, goToChannel, license, loginId, password, serverUrl, theme}
autoCapitalize='none'
autoCorrect={false}
keyboardType='numeric'
placeholder={formatMessage({id: t('login_mfa.token'), defaultMessage: 'MFA Token'})}
placeholder={formatMessage({id: 'login_mfa.token', defaultMessage: 'MFA Token'})}
placeholderTextColor={changeOpacity(theme.centerChannelColor, 0.5)}
returnKeyType='go'
underlineColorAndroid='transparent'

View file

@ -9,7 +9,7 @@ import {renderWithIntl} from '@test/intl-test-helper';
import Mfa from './index';
jest.mock('@requests/remote/user', () => {
jest.mock('@actions/remote/user', () => {
return {
login: jest.fn(),
};

View file

@ -1,11 +1,11 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Appearance, Keyboard, Platform} from 'react-native';
import {Appearance, DeviceEventEmitter, Keyboard, Platform} from 'react-native';
import {Layout, Navigation, Options, OptionsModalPresentationStyle} from 'react-native-navigation';
import merge from 'deepmerge';
import {Screens, Preferences} from '@constants';
import {Navigation as NavigationConstants, Preferences, Screens} from '@constants';
import EphemeralStore from '@store/ephemeral_store';
@ -234,7 +234,7 @@ export async function dismissAllModalsAndPopToRoot() {
await dismissAllModals();
await popToRoot();
// EventEmmiter.emit(NavigationTypes.NAVIGATION_DISMISS_AND_POP_TO_ROOT);
DeviceEventEmitter.emit(NavigationConstants.NAVIGATION_DISMISS_AND_POP_TO_ROOT);
}
export function showModal(name: string, title: string, passProps = {}, options = {}) {

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ManagedConfig, useManagedConfig} from '@mattermost/react-native-emm';
import {useManagedConfig, ManagedConfig} from '@mattermost/react-native-emm';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {useIntl} from 'react-intl';
import {
@ -9,15 +9,16 @@ import {
Platform, StatusBar, StyleSheet, TextInput, TouchableWithoutFeedback, View,
} from 'react-native';
import Button from 'react-native-button';
import {NavigationFunctionComponent} from 'react-native-navigation';
import {Navigation, NavigationFunctionComponent} from 'react-native-navigation';
import {SafeAreaView} from 'react-native-safe-area-context';
import {doPing, fetchConfigAndLicense} from '@actions/remote/general';
import LocalConfig from '@assets/config.json';
import AppVersion from '@components/app_version';
import ErrorText, {ClientErrorWithIntl} from '@components/error_text';
import FormattedText from '@components/formatted_text';
import {Screens} from '@constants';
import {doPing, fetchConfigAndLicense} from '@requests/remote/general';
import NetworkManager from '@init/network_manager';
import {goToScreen} from '@screens/navigation';
import {isMinimumServerVersion} from '@utils/helpers';
import {preventDoubleTap} from '@utils/tap';
@ -33,7 +34,7 @@ interface ServerProps extends LaunchProps {
let cancelPing: undefined | (() => void);
const Server: NavigationFunctionComponent = ({extra, launchType, launchError, theme}: ServerProps) => {
const Server: NavigationFunctionComponent = ({componentId, extra, launchType, launchError, theme}: ServerProps) => {
// TODO: If we have LaunchProps, ensure they get passed along to subsequent screens
// so that they are eventually accessible in the Channel screen.
@ -51,7 +52,7 @@ const Server: NavigationFunctionComponent = ({extra, launchType, launchError, th
const styles = getStyleSheet(theme);
const {formatMessage} = intl;
const displayLogin = (config: ClientConfig, license: ClientLicense) => {
const displayLogin = (serverUrl: string, config: ClientConfig, license: ClientLicense) => {
const samlEnabled = config.EnableSaml === 'true' && license.IsLicensed === 'true' && license.SAML === 'true';
const gitlabEnabled = config.EnableSignUpWithGitLab === 'true';
const googleEnabled = config.EnableSignUpWithGoogle === 'true' && license.IsLicensed === 'true';
@ -74,9 +75,12 @@ const Server: NavigationFunctionComponent = ({extra, launchType, launchError, th
const passProps = {
config,
extra,
launchError,
launchType,
license,
theme,
serverUrl: url,
serverUrl,
};
const defaultOptions = {
@ -89,6 +93,7 @@ const Server: NavigationFunctionComponent = ({extra, launchType, launchError, th
goToScreen(screen, title, passProps, defaultOptions);
setConnecting(false);
setUrl(serverUrl);
};
const handleConnect = preventDoubleTap((manualUrl?: string) => {
@ -133,38 +138,39 @@ const Server: NavigationFunctionComponent = ({extra, launchType, launchError, th
};
const serverUrl = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp);
const result = await doPing(serverUrl);
if (canceled) {
return;
}
if (result.error && retryWithHttp) {
const nurl = serverUrl.replace('https:', 'http:');
pingServer(nurl, false);
return;
} else if (result.error) {
setError(result.error);
setConnecting(false);
if (result.error) {
if (retryWithHttp) {
const nurl = serverUrl.replace('https:', 'http:');
pingServer(nurl, false);
} else {
setError(result.error);
setConnecting(false);
}
return;
}
const data = await fetchConfigAndLicense();
const data = await fetchConfigAndLicense(serverUrl);
if (data.error) {
setError(data.error);
setConnecting(false);
return;
}
displayLogin(data.config!, data.license!);
displayLogin(serverUrl, data.config!, data.license!);
};
const blur = useCallback(() => {
input.current?.blur();
}, []);
const handleTextChanged = useCallback((text) => {
const handleTextChanged = useCallback((text: string) => {
setUrl(text);
}, []);
@ -208,6 +214,19 @@ const Server: NavigationFunctionComponent = ({extra, launchType, launchError, th
}
}, []);
useEffect(() => {
const listener = {
componentDidAppear: () => {
if (url) {
NetworkManager.invalidateClient(url);
}
},
};
const unsubscribe = Navigation.events().registerComponentListener(listener, componentId);
return () => unsubscribe.remove();
}, [componentId, url]);
let buttonIcon;
let buttonText;

View file

@ -5,26 +5,26 @@ import {useManagedConfig} from '@mattermost/react-native-emm';
import React, {useState} from 'react';
import {useIntl} from 'react-intl';
import {Client4} from '@client/rest';
import {SSO as SSOEnum} from '@constants';
import {scheduleExpiredNotification} from '@requests/remote/push_notification';
import {ssoLogin} from '@requests/remote/user';
import {scheduleExpiredNotification} from '@actions/remote/push_notification';
import {ssoLogin} from '@actions/remote/user';
import {resetToChannel} from '@screens/navigation';
import {ErrorApi} from '@typings/api/client4';
import {ErrorApi} from '@typings/api/client';
import {isMinimumServerVersion} from '@utils/helpers';
import type {LaunchProps} from '@typings/launch';
import SSOWithRedirectURL from './sso_with_redirect_url';
import SSOWithWebView from './sso_with_webview';
interface SSOProps {
interface SSOProps extends LaunchProps {
config: Partial<ClientConfig>;
license: Partial<ClientLicense>;
serverUrl: string;
ssoType: string;
theme: Partial<Theme>;
}
const SSO = ({config, serverUrl, ssoType, theme}: SSOProps) => {
const SSO = ({config, extra, launchError, launchType, serverUrl, ssoType, theme}: SSOProps) => {
const intl = useIntl();
const managedConfig = useManagedConfig();
@ -70,10 +70,8 @@ const SSO = ({config, serverUrl, ssoType, theme}: SSOProps) => {
setLoginError(errorMessage);
};
const onMMToken = async (token: string) => {
Client4.setToken(token);
const {error = undefined} = await ssoLogin(serverUrl);
const doSSOLogin = async (bearerToken: string, csrfToken: string) => {
const {error = undefined} = await ssoLogin(serverUrl!, bearerToken, csrfToken);
if (error) {
onLoadEndError(error);
setLoginError(error);
@ -83,19 +81,16 @@ const SSO = ({config, serverUrl, ssoType, theme}: SSOProps) => {
};
const goToChannel = () => {
scheduleExpiredNotification(serverUrl, intl);
resetToChannel();
scheduleExpiredNotification(serverUrl!, intl);
resetToChannel({extra, launchError, launchType, serverUrl});
};
const isSSOWithRedirectURLAvailable = isMinimumServerVersion(config.Version!, 5, 33, 0);
const props = {
doSSOLogin,
loginError,
loginUrl,
onCSRFToken: (csrfToken: string) => {
Client4.setCSRF(csrfToken);
},
onMMToken,
setLoginError,
theme,
};
@ -105,12 +100,17 @@ const SSO = ({config, serverUrl, ssoType, theme}: SSOProps) => {
<SSOWithWebView
{...props}
completeUrlPath={completeUrlPath}
serverUrl={serverUrl}
serverUrl={serverUrl!}
ssoType={ssoType}
/>
);
}
return <SSOWithRedirectURL {...props}/>;
return (
<SSOWithRedirectURL
{...props}
serverUrl={serverUrl!}
/>
);
};
export default SSO;

View file

@ -5,6 +5,7 @@ import React from 'react';
import {Preferences} from '@constants';
import {renderWithIntl} from '@test/intl-test-helper';
import {LaunchType} from '@typings/launch';
import SSOLogin from './index';
@ -28,6 +29,7 @@ describe('SSO', () => {
ssoType: 'GITLAB',
theme: Preferences.THEMES.default,
serverUrl: 'https://locahost:8065',
launchType: LaunchType.Normal,
};
test('implement with webview when version is less than 5.32 version', async () => {

View file

@ -17,11 +17,11 @@ jest.mock('@utils/url', () => {
describe('SSO with redirect url', () => {
const baseProps = {
customUrlScheme: 'mmauth://',
doSSOLogin: jest.fn(),
intl: {},
loginError: '',
loginUrl: '',
onCSRFToken: jest.fn(),
onMMToken: jest.fn(),
serverUrl: 'http://localhost:8065',
setLoginError: jest.fn(),
theme: Preferences.THEMES.default,
};

View file

@ -9,32 +9,36 @@ import urlParse from 'url-parse';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {REDIRECT_URL_SCHEME, REDIRECT_URL_SCHEME_DEV} from '@constants';
import NetworkManager from '@app/init/network_manager';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {tryOpenURL} from '@utils/url';
interface SSOWithRedirectURLProps {
doSSOLogin: (bearerToken: string, csrfToken: string) => void;
loginError: string;
loginUrl: string;
onCSRFToken: (token: string) => void;
onMMToken: (token: string) => void;
serverUrl: string;
setLoginError: (value: string) => void;
theme: Partial<Theme>
}
const SSOWithRedirectURL = ({loginError, loginUrl, onCSRFToken, onMMToken, setLoginError, theme}: SSOWithRedirectURLProps) => {
const SSOWithRedirectURL = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOWithRedirectURLProps) => {
const [error, setError] = useState<string>('');
const style = getStyleSheet(theme);
const intl = useIntl();
let customUrlScheme = 'mmauth://';
let customUrlScheme = REDIRECT_URL_SCHEME;
if (DeviceInfo.getBundleId && DeviceInfo.getBundleId().includes('rnbeta')) {
customUrlScheme = 'mmauthbeta://';
customUrlScheme = REDIRECT_URL_SCHEME_DEV;
}
const redirectUrl = customUrlScheme + 'callback';
const init = (resetErrors?: boolean) => {
const init = (resetErrors = true) => {
if (resetErrors !== false) {
setError('');
setLoginError('');
NetworkManager.invalidateClient(serverUrl);
NetworkManager.createClient(serverUrl);
}
const parsedUrl = urlParse(loginUrl, true);
parsedUrl.set('query', {
@ -60,6 +64,7 @@ const SSOWithRedirectURL = ({loginError, loginUrl, onCSRFToken, onMMToken, setLo
message,
);
};
tryOpenURL(url, onError);
};
@ -67,9 +72,10 @@ const SSOWithRedirectURL = ({loginError, loginUrl, onCSRFToken, onMMToken, setLo
const onURLChange = ({url}: { url: string }) => {
if (url && url.startsWith(redirectUrl)) {
const parsedUrl = urlParse(url, true);
if (parsedUrl.query && parsedUrl.query.MMCSRF && parsedUrl.query.MMAUTHTOKEN) {
onCSRFToken(parsedUrl.query.MMCSRF);
onMMToken(parsedUrl.query.MMAUTHTOKEN);
const bearerToken = parsedUrl.query?.MMAUTHTOKEN;
const csrfToken = parsedUrl.query?.MMCSRF;
if (bearerToken && csrfToken) {
doSSOLogin(bearerToken, csrfToken);
} else {
setError(
intl.formatMessage({
@ -117,14 +123,6 @@ const SSOWithRedirectURL = ({loginError, loginUrl, onCSRFToken, onMMToken, setLo
defaultMessage='Please use your browser to complete the login'
style={style.infoText}
/>
<TouchableOpacity onPress={() => init()}>
<FormattedText
id='mobile.oauth.restart_login'
testID='mobile.oauth.restart_login'
defaultMessage='Restart login'
style={style.button}
/>
</TouchableOpacity>
<Loading/>
</View>
)}

View file

@ -1,10 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import CookieManager, {Cookies} from '@react-native-community/cookies';
import CookieManager, {Cookies} from '@react-native-cookies/cookies';
import React, {useEffect} from 'react';
import {useIntl} from 'react-intl';
import {Alert, Text, View} from 'react-native';
import {Alert, Platform, Text, View} from 'react-native';
import {SafeAreaView} from 'react-native-safe-area-context';
import {WebView} from 'react-native-webview';
import {
@ -15,7 +15,6 @@ import {
} from 'react-native-webview/lib/WebViewTypes';
import urlParse from 'url-parse';
import {Client4} from '@client/rest';
import Loading from '@components/loading';
import {SSO} from '@constants';
import {popTopScreen} from '@screens/navigation';
@ -59,20 +58,20 @@ const oneLoginFormScalingJS = `
interface SSOWithWebViewProps {
completeUrlPath: string;
doSSOLogin: (bearerToken: string, csrfToken: string) => void;
loginError: string;
loginUrl: string;
onCSRFToken: (token: string) => void;
onMMToken: (token: string) => void;
serverUrl: string;
ssoType: string;
theme: Partial<Theme>
}
const SSOWithWebView = ({completeUrlPath, loginError, loginUrl, onCSRFToken, onMMToken, serverUrl, ssoType, theme}: SSOWithWebViewProps) => {
const SSOWithWebView = ({completeUrlPath, doSSOLogin, loginError, loginUrl, serverUrl, ssoType, theme}: SSOWithWebViewProps) => {
const style = getStyleSheet(theme);
const intl = useIntl();
const [error, setError] = React.useState(null);
const [jsCode, setJSCode] = React.useState('');
const visitedUrls = React.useRef(new Set<string>()).current;
const [messagingEnabled, setMessagingEnabled] = React.useState(false);
const [shouldRenderWebView, setShouldRenderWebView] = React.useState(true);
const cookiesTimeout = React.useRef<NodeJS.Timeout>();
@ -86,6 +85,21 @@ const SSOWithWebView = ({completeUrlPath, loginError, loginUrl, onCSRFToken, onM
};
}, []);
const removeCookiesFromVisited = () => {
if (Platform.OS === 'ios') {
visitedUrls.forEach((visited) => {
CookieManager.get(visited, true).then(async (cookies: Cookies) => {
if (cookies) {
const values = Object.values(cookies);
for (const cookie of values) {
CookieManager.clearByName(visited, cookie.name, true);
}
}
});
});
}
};
const extractCookie = (parsedUrl: urlParse) => {
try {
const original = urlParse(serverUrl);
@ -95,19 +109,16 @@ const SSOWithWebView = ({completeUrlPath, loginError, loginUrl, onCSRFToken, onM
// Rebuild the server url without query string and/or hash
const url = `${parsedUrl.origin}${parsedUrl.pathname}`;
Client4.setUrl(url);
CookieManager.get(url, true).then((res: Cookies) => {
const mmtoken = res.MMAUTHTOKEN;
const csrf = res.MMCSRF;
const token = typeof mmtoken === 'object' ? mmtoken.value : mmtoken;
const bearerToken = typeof mmtoken === 'object' ? mmtoken.value : mmtoken;
const csrfToken = typeof csrf === 'object' ? csrf.value : csrf;
if (csrfToken) {
onCSRFToken(csrfToken);
}
if (token) {
onMMToken(token);
if (bearerToken) {
removeCookiesFromVisited();
doSSOLogin(bearerToken, csrfToken);
if (cookiesTimeout.current) {
clearTimeout(cookiesTimeout.current);
}
@ -162,6 +173,10 @@ const SSOWithWebView = ({completeUrlPath, loginError, loginUrl, onCSRFToken, onM
const {url} = navState;
let isMessagingEnabled = false;
const parsed = urlParse(url);
if (!serverUrl.includes(parsed.host)) {
visitedUrls.add(parsed.origin);
}
if (parsed.host.includes('.onelogin.com')) {
setJSCode(oneLoginFormScalingJS);
} else if (parsed.pathname === completeUrlPath) {
@ -193,6 +208,7 @@ const SSOWithWebView = ({completeUrlPath, loginError, loginUrl, onCSRFToken, onM
<WebView
automaticallyAdjustContentInsets={false}
cacheEnabled={true}
incognito={Platform.OS === 'android'}
injectedJavaScript={jsCode}
javaScriptEnabled={true}
onLoadEnd={onLoadEnd}

View file

@ -4,10 +4,11 @@
import {Platform} from 'react-native';
import {FileSystem} from 'react-native-unimodules';
const vectorIconsDir = 'vectorIcons';
import {hashCode} from './security';
export async function deleteFileCache() {
const cacheDir = FileSystem.cacheDirectory;
export async function deleteFileCache(serverUrl: string) {
const serverDir = hashCode(serverUrl);
const cacheDir = `${FileSystem.cacheDirectory}${serverDir}`;
if (cacheDir) {
const cacheDirInfo = await FileSystem.getInfoAsync(cacheDir);
if (cacheDirInfo.exists) {
@ -17,9 +18,7 @@ export async function deleteFileCache() {
} else {
const lstat = await FileSystem.readDirectoryAsync(cacheDir);
lstat.forEach((stat: string) => {
if (!stat.includes(vectorIconsDir)) {
FileSystem.deleteAsync(stat, {idempotent: true});
}
FileSystem.deleteAsync(stat, {idempotent: true});
});
}
}

View file

@ -6,6 +6,7 @@
// versions, and a non-equal minor version will ignore dot version.
// currentVersion is a string, e.g '4.6.0'
// minMajorVersion, minMinorVersion, minDotVersion are integers
export const isMinimumServerVersion = (currentVersion: string, minMajorVersion = 0, minMinorVersion = 0, minDotVersion = 0): boolean => {
if (!currentVersion || typeof currentVersion !== 'string') {
return false;
@ -72,3 +73,16 @@ export function isEmail(email: string): boolean {
// this prevents <Outlook Style> outlook.style@domain.com addresses and multiple comma-separated addresses from being accepted
return (/^[^ ,@]+@[^ ,@]+$/).test(email);
}
export function safeParseJSON(rawJson: string | Record<string, unknown>) {
let data = rawJson;
try {
if (typeof rawJson == 'string') {
data = JSON.parse(rawJson);
}
} catch {
// Do nothing
}
return data;
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import CookieManager from '@react-native-community/cookies';
import CookieManager from '@react-native-cookies/cookies';
export async function getCSRFFromCookie(url: string) {
const cookies = await CookieManager.get(url, false);

View file

@ -4,6 +4,8 @@
import {Linking} from 'react-native';
import urlParse from 'url-parse';
import GenericClient from '@mattermost/react-native-network-client';
import {Files} from '@constants';
import {DeepLinkType, DeepLinkWithData} from '@typings/launch';
import {emptyFunction} from '@utils/general';
@ -39,12 +41,12 @@ export async function getServerUrlAfterRedirect(serverUrl: string, useHttp = fal
let url = sanitizeUrl(serverUrl, useHttp);
try {
const resp = await fetch(url, {method: 'HEAD'});
if (resp.redirected) {
url = resp.url;
const resp = await GenericClient.head(url);
if (resp.redirectUrls?.length) {
url = resp.redirectUrls[resp.redirectUrls.length - 1];
}
} catch {
// do nothing
} catch (error) {
// do nothing
}
return sanitizeUrl(url, useHttp);

View file

@ -1,7 +1,7 @@
{
"login_mfa.enterToken": "To complete the sign in process, please enter a token from your smartphone's authenticator",
"login_mfa.token": "",
"login_mfa.tokenReq": "",
"login_mfa.token": "MFA Token",
"login_mfa.tokenReq": "Please enter an MFA token",
"login.email": "Email",
"login.forgot": "I forgot my password",
"login.invalidPassword": "Your password is incorrect.",
@ -50,6 +50,7 @@
"mobile.push_notification_reply.button": "Send",
"mobile.push_notification_reply.placeholder": "Write a reply...",
"mobile.push_notification_reply.title": "Reply",
"mobile.request.invalid_request_method": "Invalid request method",
"mobile.request.invalid_response": "Received invalid response from the server.",
"mobile.routes.login": "Login",
"mobile.routes.loginOptions": "Login Chooser",

View file

@ -19,6 +19,7 @@ module.exports = {
['module-resolver', {
root: ['.'],
alias: {
'@actions': './app/actions',
'@app': './app/',
'@assets': './dist/assets',
'@client': './app/client',
@ -29,7 +30,6 @@ module.exports = {
'@init': './app/init',
'@notifications': './app/notifications',
'@queries': './app/queries',
'@requests': './app/requests',
'@screens': './app/screens',
'@share': './share_extension',
'@store': './app/store',

View file

@ -6,11 +6,13 @@ import 'react-native-gesture-handler';
import {ComponentDidAppearEvent, ComponentDidDisappearEvent, Navigation} from 'react-native-navigation';
import {Navigation as NavigationConstants, Screens} from './app/constants';
import DatabaseManager from './app/database/manager';
import {getAllServerCredentials} from './app/init/credentials';
import GlobalEventHandler from './app/init/global_event_handler';
import './app/init/fetch';
import {initialLaunch} from './app/init/launch';
import NetworkManager from './app/init/network_manager';
import ManagedApp from './app/init/managed_app';
import PushNotifications from './app/init/push_notifications';
import {registerScreens} from './app/screens/index';
import EphemeralStore from './app/store/ephemeral_store';
import setFontFamily from './app/utils/font_family';
@ -23,6 +25,7 @@ if (__DEV__) {
'`-[RCTRootView cancelTouches]`',
'scaleY',
]);
LogBox.ignoreLogs(['Require cycle: node_modules/zod/lib/src/index.js']);
require('storybook/mattermost_storybook.ts');
}
@ -50,12 +53,12 @@ Navigation.events().registerAppLaunchedListener(async () => {
registerNavigationListeners();
registerScreens();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const serverCredentials = await getAllServerCredentials();
const serverUrls = serverCredentials.map((credential) => credential.serverUrl);
// TODO:
// DatabaseManager.init(serverCredentials);
// NetworkClientManager.init(serverCredentials);
await DatabaseManager.init(serverUrls);
await NetworkManager.init(serverCredentials);
PushNotifications.init();
initialLaunch();
});

View file

@ -30,7 +30,7 @@ extension DatabaseError: LocalizedError {
}
public class DatabaseHelper: NSObject {
internal let DEFAULT_DB_NAME = "default.db"
internal let DEFAULT_DB_NAME = "app.db"
internal var DEFAULT_DB_PATH: String
internal var defaultDB: OpaquePointer? = nil
@ -64,7 +64,7 @@ public class DatabaseHelper: NSObject {
}
var queryStatement: OpaquePointer?
let queryString = "SELECT url FROM servers;"
let queryString = "SELECT url FROM Servers;"
if sqlite3_prepare_v2(defaultDB, queryString, -1, &queryStatement, nil) == SQLITE_OK {
if sqlite3_step(queryStatement) == SQLITE_ROW,
let result = sqlite3_column_text(queryStatement, 0) {

View file

@ -26,6 +26,9 @@ target 'Mattermost' do
pod 'XCDYouTubeKit', '2.8.2'
pod 'Swime', '3.0.6'
# TODO: Remove this once upstream PR https://github.com/daltoniam/Starscream/pull/871 is merged
pod 'Starscream', :git => 'https://github.com/mattermost/Starscream.git', :commit => '1b4b93708fb63d2665625a11e57461772a65364a'
end
# Enables Flipper.

View file

@ -1,4 +1,5 @@
PODS:
- Alamofire (5.4.3)
- boost-for-react-native (1.63.0)
- BVLinearGradient (2.5.6):
- React
@ -231,7 +232,7 @@ PODS:
- React-jsinspector (0.64.2)
- react-native-cameraroll (4.0.4):
- React-Core
- react-native-cookies (5.0.1):
- react-native-cookies (6.0.8):
- React-Core
- react-native-document-picker (5.2.0):
- React-Core
@ -241,6 +242,11 @@ PODS:
- React
- react-native-netinfo (6.0.0):
- React-Core
- react-native-network-client (0.1.0):
- Alamofire (~> 5.4)
- React-Core
- Starscream (~> 4.0.4)
- SwiftyJSON (~> 5.0)
- react-native-notifications (3.5.0):
- React-Core
- react-native-paste-input (0.1.3):
@ -403,7 +409,7 @@ PODS:
- React
- RNVectorIcons (8.1.0):
- React-Core
- Rudder (1.0.18)
- Rudder (1.0.14)
- SDWebImage (5.11.1):
- SDWebImage/Core (= 5.11.1)
- SDWebImage/Core (5.11.1)
@ -413,6 +419,8 @@ PODS:
- Sentry (7.0.0):
- Sentry/Core (= 7.0.0)
- Sentry/Core (7.0.0)
- Starscream (4.0.4)
- SwiftyJSON (5.0.1)
- Swime (3.0.6)
- UMAppLoader (2.1.0)
- UMBarCodeScannerInterface (6.1.0):
@ -421,7 +429,7 @@ PODS:
- UMCore
- UMConstantsInterface (6.1.0):
- UMCore
- UMCore (7.1.0)
- UMCore (7.1.1)
- UMFaceDetectorInterface (6.1.0)
- UMFileSystemInterface (6.1.0)
- UMFontInterface (6.1.0)
@ -469,11 +477,12 @@ DEPENDENCIES:
- React-jsiexecutor (from `../node_modules/react-native/ReactCommon/jsiexecutor`)
- React-jsinspector (from `../node_modules/react-native/ReactCommon/jsinspector`)
- "react-native-cameraroll (from `../node_modules/@react-native-community/cameraroll`)"
- "react-native-cookies (from `../node_modules/@react-native-community/cookies`)"
- "react-native-cookies (from `../node_modules/@react-native-cookies/cookies`)"
- react-native-document-picker (from `../node_modules/react-native-document-picker`)
- "react-native-emm (from `../node_modules/@mattermost/react-native-emm`)"
- react-native-hw-keyboard-event (from `../node_modules/react-native-hw-keyboard-event`)
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
- "react-native-network-client (from `../node_modules/@mattermost/react-native-network-client`)"
- react-native-notifications (from `../node_modules/react-native-notifications`)
- "react-native-paste-input (from `../node_modules/@mattermost/react-native-paste-input`)"
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
@ -513,6 +522,7 @@ DEPENDENCIES:
- RNShare (from `../node_modules/react-native-share`)
- RNSVG (from `../node_modules/react-native-svg`)
- RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- Starscream (from `https://github.com/mattermost/Starscream.git`, commit `1b4b93708fb63d2665625a11e57461772a65364a`)
- Swime (= 3.0.6)
- UMAppLoader (from `../node_modules/unimodules-app-loader/ios`)
- UMBarCodeScannerInterface (from `../node_modules/unimodules-barcode-scanner-interface/ios`)
@ -533,12 +543,14 @@ DEPENDENCIES:
SPEC REPOS:
trunk:
- Alamofire
- boost-for-react-native
- libwebp
- Rudder
- SDWebImage
- SDWebImageWebPCoder
- Sentry
- SwiftyJSON
- Swime
- XCDYouTubeKit
- YoutubePlayer-in-WKWebView
@ -591,7 +603,7 @@ EXTERNAL SOURCES:
react-native-cameraroll:
:path: "../node_modules/@react-native-community/cameraroll"
react-native-cookies:
:path: "../node_modules/@react-native-community/cookies"
:path: "../node_modules/@react-native-cookies/cookies"
react-native-document-picker:
:path: "../node_modules/react-native-document-picker"
react-native-emm:
@ -600,6 +612,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-hw-keyboard-event"
react-native-netinfo:
:path: "../node_modules/@react-native-community/netinfo"
react-native-network-client:
:path: "../node_modules/@mattermost/react-native-network-client"
react-native-notifications:
:path: "../node_modules/react-native-notifications"
react-native-paste-input:
@ -678,6 +692,9 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-svg"
RNVectorIcons:
:path: "../node_modules/react-native-vector-icons"
Starscream:
:commit: 1b4b93708fb63d2665625a11e57461772a65364a
:git: https://github.com/mattermost/Starscream.git
UMAppLoader:
:path: "../node_modules/unimodules-app-loader/ios"
UMBarCodeScannerInterface:
@ -709,14 +726,20 @@ EXTERNAL SOURCES:
Yoga:
:path: "../node_modules/react-native/ReactCommon/yoga"
CHECKOUT OPTIONS:
Starscream:
:commit: 1b4b93708fb63d2665625a11e57461772a65364a
:git: https://github.com/mattermost/Starscream.git
SPEC CHECKSUMS:
Alamofire: e447a2774a40c996748296fa2c55112fdbbc42f9
boost-for-react-native: 39c7adb57c4e60d6c5479dd8623128eb5b3f0f2c
BVLinearGradient: e3aad03778a456d77928f594a649e96995f1c872
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
EXConstants: c4dd28acc12039c999612507a5f935556f2c86ce
EXFileSystem: dcf2273f49431e5037347c733a2dc5d08e0d0a9e
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: d35931295aacfe996e833c01a3701d4aa7a80cb4
FBReactNativeSpec: cef0cc6d50abc92e8cf52f140aa22b5371cfec0b
glog: 73c2498ac6884b13ede40eda8228cb1eee9d9d62
jail-monkey: 01cd0a75aa1034d08fd851869e6e6c3b063242d7
libwebp: e90b9c01d99205d03b6bb8f2c8c415e5a4ef66f0
@ -735,11 +758,12 @@ SPEC CHECKSUMS:
React-jsiexecutor: 80c46bd381fd06e418e0d4f53672dc1d1945c4c3
React-jsinspector: cc614ec18a9ca96fd275100c16d74d62ee11f0ae
react-native-cameraroll: 88f4e62d9ecd0e1f253abe4f685474f2ea14bfa2
react-native-cookies: ce50e42ace7cf0dd47769260ca5bbe8eee607e4e
react-native-cookies: 2cb6ef472da68610dfcf0eaee68464c244943abd
react-native-document-picker: f1b5398801b332c77bc62ae0eae2116f49bdff26
react-native-emm: 1652c0f3ebc39ca0cb57a43f60b96f932b7b7f19
react-native-hw-keyboard-event: b517cefb8d5c659a38049c582de85ff43337dc53
react-native-netinfo: e849fc21ca2f4128a5726c801a82fc6f4a6db50d
react-native-network-client: 30ab97e7e6c8d6f2d2b10cc1ebad0cbf9c894c6e
react-native-notifications: 89a73cd2cd2648e1734fa10e3507681c9e4f14de
react-native-paste-input: dbf0099efd191ddf53e55a4a454bea0783ce4e1d
react-native-safe-area-context: f0906bf8bc9835ac9a9d3f97e8bde2a997d8da79
@ -779,16 +803,18 @@ SPEC CHECKSUMS:
RNShare: 5ac8f6532ca4cd80fc71caef1cfbba1854a6a045
RNSVG: 551acb6562324b1d52a4e0758f7ca0ec234e278f
RNVectorIcons: 31cebfcf94e8cf8686eb5303ae0357da64d7a5a4
Rudder: 2a70cad66a3f5f3f6be056bf1ce0ce8d10b5ca67
Rudder: 40f3a255fab3f8bbe120e496f90019de68c1aca1
SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d
SDWebImageWebPCoder: d0dac55073088d24b2ac1b191a71a8f8d0adac21
Sentry: 89d26e036063b9cb9caa59b6951dd2f8277aa13b
Starscream: 5178aed56b316f13fa3bc55694e583d35dd414d9
SwiftyJSON: 2f33a42c6fbc52764d96f13368585094bfd8aa5e
Swime: d7b2c277503b6cea317774aedc2dce05613f8b0b
UMAppLoader: fe2708bb0ac5cd70052bc207d06aa3b7e72b9e97
UMBarCodeScannerInterface: 79f92bea5f7af39b381a4c82298105ceb537408a
UMCameraInterface: 81ff46700da88435f17afedfc88915eaede7e6a6
UMConstantsInterface: bb94dd46039dcde276ed50225b29e22785e604bf
UMCore: 60b35f4d217461f7b54934b0c5be67442871f01f
UMCore: 2f671796d7439604a1cf8ac7bbe5809cd5c50437
UMFaceDetectorInterface: 791eec55ffca1171992976b7eceb73e69e391c58
UMFileSystemInterface: f72245e90ce78fa6427180ff0b0904ead13d8161
UMFontInterface: 5843cff7db85a42ba629aaac53d33091c35524d3
@ -802,6 +828,6 @@ SPEC CHECKSUMS:
Yoga: 575c581c63e0d35c9a83f4b46d01d63abc1100ac
YoutubePlayer-in-WKWebView: cfbf46da51d7370662a695a8f351e5fa1d3e1008
PODFILE CHECKSUM: 34335a8d329b001fdfa695c4fb3c8928db80f375
PODFILE CHECKSUM: 7741358261bad1ae9b7877b2299303396085e050
COCOAPODS: 1.10.1

View file

@ -20,6 +20,6 @@ module.exports = {
'<rootDir>/dist/assets/images/video_player/$1@2x.png',
},
transformIgnorePatterns: [
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-navigation|@react-native-community/cameraroll|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|sentry-expo|native-base|unimodules-permissions-interface)',
'node_modules/(?!(@react-native|react-native)|jail-monkey|@sentry/react-native|react-navigation|@react-native-community/cameraroll|react-clone-referenced-element|@react-native-community|expo(nent)?|@expo(nent)?/.*|react-navigation|@react-navigation/.*|@unimodules/.*|sentry-expo|native-base|unimodules-permissions-interface|validator)',
],
};

2780
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -15,13 +15,14 @@
"@formatjs/intl-pluralrules": "4.0.27",
"@formatjs/intl-relativetimeformat": "9.1.6",
"@mattermost/react-native-emm": "1.1.3",
"@mattermost/react-native-network-client": "github:mattermost/react-native-network-client",
"@mattermost/react-native-paste-input": "0.1.3",
"@nozbe/watermelondb": "0.22.0",
"@nozbe/with-observables": "1.3.0",
"@react-native-community/async-storage": "1.12.1",
"@react-native-community/cameraroll": "4.0.4",
"@react-native-community/clipboard": "1.5.1",
"@react-native-community/cookies": "5.0.1",
"@react-native-cookies/cookies": "6.0.8",
"@react-native-community/masked-view": "0.1.11",
"@react-native-community/netinfo": "6.0.0",
"@rudderstack/rudder-sdk-react-native": "1.0.10",

View file

@ -1,8 +1,8 @@
diff --git a/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m b/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m
index 38ccdf3..bdc4713 100644
index 38ccdf3..3c80299 100644
--- a/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m
+++ b/node_modules/react-native-keychain/RNKeychainManager/RNKeychainManager.m
@@ -272,6 +272,34 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
@@ -272,6 +272,37 @@ SecAccessControlCreateFlags accessControlValue(NSDictionary *options)
return SecItemDelete((__bridge CFDictionaryRef) query);
}
@ -25,8 +25,11 @@ index 38ccdf3..bdc4713 100644
+ result = (__bridge NSArray*)(resultRef);
+ if (result != NULL) {
+ for (id entry in result) {
+ NSString *server = [entry objectForKey:(__bridge NSString *)kSecAttrServer];
+ [servers addObject:server];
+ NSMutableData *serverData = [entry objectForKey:(__bridge NSString *)kSecAttrServer];
+ if (serverData != NULL) {
+ NSString *server = [[NSString alloc] initWithData:serverData encoding:NSUTF8StringEncoding];
+ [servers addObject:server];
+ }
+ }
+ }
+ }
@ -37,7 +40,7 @@ index 38ccdf3..bdc4713 100644
-(NSArray<NSString*>*)getAllServicesForSecurityClasses:(NSArray *)secItemClasses
{
NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
@@ -592,4 +620,14 @@ - (OSStatus)deleteCredentialsForServer:(NSString *)server
@@ -592,4 +623,14 @@ RCT_EXPORT_METHOD(getAllGenericPasswordServices:(RCTPromiseResolveBlock)resolve
}
}
@ -53,7 +56,7 @@ index 38ccdf3..bdc4713 100644
+
@end
diff --git a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
index 3da433f..112cc74 100644
index 3da433f..731b991 100644
--- a/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
+++ b/node_modules/react-native-keychain/android/src/main/java/com/oblador/keychain/KeychainModule.java
@@ -1,6 +1,7 @@
@ -111,6 +114,14 @@ index 3da433f..112cc74 100644
promise.resolve(credentials);
} catch (KeyStoreAccessException e) {
@@ -372,6 +381,7 @@ public class KeychainModule extends ReactContextBaseJavaModule {
}
// And then we remove the entry in the shared preferences
prefsStorage.removeEntry(alias);
+ cachedCredentialsMap.remove(alias);
promise.resolve(true);
} catch (KeyStoreAccessException e) {
diff --git a/node_modules/react-native-keychain/index.js b/node_modules/react-native-keychain/index.js
index b73cfb2..a754505 100644
--- a/node_modules/react-native-keychain/index.js

View file

@ -96,6 +96,36 @@ jest.doMock('react-native', () => {
},
}),
},
APIClient: {
getConstants: () => ({
EVENTS: {
UPLOAD_PROGRESS: 'APIClient-UploadProgress',
CLIENT_ERROR: 'APIClient-Error',
},
RETRY_TYPES: {
EXPONENTIAL_RETRY: 'exponential',
LINEAR_RETRY: 'linear',
},
}),
},
WebSocketClient: {
getConstants: () => ({
EVENTS: {
OPEN_EVENT: 'WebSocketClient-Open',
CLOSE_EVENT: 'WebSocketClient-Close',
ERROR_EVENT: 'WebSocketClient-Error',
MESSAGE_EVENT: 'WebSocketClient-Message',
READY_STATE_EVENT: 'WebSocketClient-ReadyState',
CLIENT_ERROR: 'WebSocketClient-Error',
},
READY_STATE: {
CONNECTING: 0,
OPEN: 1,
CLOSING: 2,
CLOSED: 3,
},
}),
},
};
const Linking = {
@ -169,6 +199,7 @@ jest.mock('react-native-device-info', () => {
hasNotch: () => true,
isTablet: () => false,
getApplicationName: () => 'Mattermost',
getUserAgent: () => 'user-agent',
};
});
@ -181,7 +212,7 @@ jest.mock('react-native-localize', () => ({
]),
}));
jest.mock('@react-native-community/cookies', () => ({
jest.mock('@react-native-cookies/cookies', () => ({
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
openURL: jest.fn(),
@ -278,6 +309,8 @@ jest.mock('@screens/navigation', () => ({
dismissOverlay: jest.fn(() => Promise.resolve()),
}));
jest.mock('@mattermost/react-native-emm');
declare const global: {requestAnimationFrame: (callback: any) => void};
global.requestAnimationFrame = (callback) => {
setTimeout(callback, 0);

View file

@ -43,11 +43,17 @@ class TestHelper {
};
createClient = () => {
const client = new Client();
const mockApiClient = {
baseUrl: 'https://community.mattermost.com',
delete: jest.fn(),
head: jest.fn(),
get: jest.fn(),
patch: jest.fn(),
post: jest.fn(),
put: jest.fn(),
};
client.setUrl(Config.DefaultServerUrl || Config.TestServerUrl);
return client;
return new Client(mockApiClient, mockApiClient.baseUrl);
};
fakeChannel = (teamId) => {

View file

@ -33,6 +33,7 @@
"resolveJsonModule": true,
"baseUrl": ".",
"paths": {
"@actions/*": ["app/actions/*"],
"@app/*": ["app/*"],
"@assets/*": ["dist/assets/*"],
"@client/*": ["app/client/*"],
@ -44,7 +45,6 @@
"@init/*": ["app/init/*"],
"@notifications": ["app/notifications/index"],
"@queries/*": ["app/queries/*"],
"@requests/*": ["app/requests/*"],
"@screens/*": ["app/screens/*"],
"@selectors/*": ["app/selectors/*"],
"@share/*": ["share_extension/*"],

View file

@ -25,11 +25,6 @@ export type ErrorApi = {
};
export type Client4Error = ErrorOffline | ErrorInvalidResponse | ErrorApi;
export type ClientOptions = {
headers?: {
[x: string]: string;
};
method?: string;
url?: string;
credentials?: 'omit' | 'same-origin' | 'include';
body?: any;
method?: string;
};

10
types/api/session.d.ts vendored Normal file
View file

@ -0,0 +1,10 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
interface Session {
id: string,
create_at: string|number;
device_id?: string;
expires_at: string|number;
user_id: string;
}