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:
parent
7ff119fdc1
commit
134c4a49c5
96 changed files with 2539 additions and 2753 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
86
app/actions/remote/general.ts
Normal file
86
app/actions/remote/general.ts
Normal 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};
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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,
|
||||
45
app/actions/remote/systems.ts
Normal file
45
app/actions/remote/systems.ts
Normal 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;
|
||||
};
|
||||
|
|
@ -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};
|
||||
};
|
||||
|
|
@ -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},
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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}},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}},
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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'},
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
);
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -68,5 +68,5 @@ export default {
|
|||
DISABLED: 'disabled',
|
||||
DEFAULT_ON: 'default_on',
|
||||
DEFAULT_OFF: 'default_off',
|
||||
REHYDRATED: 'app/REHYDRATED',
|
||||
SERVER_LOGOUT: 'server_logout',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
13
app/constants/network.ts
Normal 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,
|
||||
};
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
123
app/init/network_manager.ts
Normal 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();
|
||||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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 '';
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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};
|
||||
}
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -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.'}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = {}) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
13
index.ts
13
index.ts
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
2780
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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/*"],
|
||||
|
|
|
|||
|
|
@ -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
10
types/api/session.d.ts
vendored
Normal 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;
|
||||
}
|
||||
Loading…
Reference in a new issue