[Gekidou] [MM-41837] Add verification for push proxy and related interface (#6192)

* Add verification for push proxy and related interface

* Fix lint and extract i18n

* Be specific about undefined equalities.

* Fix test

* Address feedback

* Fix long server names styles

* Fix tests and typo
This commit is contained in:
Daniel Espino García
2022-05-09 11:41:16 +02:00
committed by GitHub
parent a80505496c
commit 7e80843092
22 changed files with 441 additions and 68 deletions

View File

@@ -6,10 +6,14 @@ import {switchToChannelById} from '@actions/remote/channel';
import {fetchRoles} from '@actions/remote/role';
import {fetchConfigAndLicense} from '@actions/remote/systems';
import {Screens} from '@constants';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
import NetworkManager from '@managers/network_manager';
import {getDeviceToken} from '@queries/app/global';
import {queryChannelsById, getDefaultChannelForTeam} from '@queries/servers/channel';
import {prepareModels} from '@queries/servers/entry';
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId} from '@queries/servers/system';
import {prepareCommonSystemValues, getCommonSystemValues, getCurrentTeamId, getWebSocketLastDisconnected, setCurrentTeamAndChannelId, getPushVerificationStatus} from '@queries/servers/system';
import {getNthLastChannelFromTeam} from '@queries/servers/team';
import {getCurrentUser} from '@queries/servers/user';
import {deleteV1Data} from '@utils/file';
@@ -96,10 +100,59 @@ export async function appEntry(serverUrl: string, since = 0) {
syncOtherServers(serverUrl);
}
verifyPushProxy(serverUrl);
const error = teamData.error || chData?.error || prefData.error || meData.error;
return {error, userId: meData?.user?.id};
}
export async function verifyPushProxy(serverUrl: string) {
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;
if (!operator) {
return;
}
const {database} = operator;
const ppVerification = await getPushVerificationStatus(database);
if (ppVerification !== PUSH_PROXY_STATUS_UNKNOWN) {
return;
}
const appDatabase = DatabaseManager.appDatabase?.database;
if (!appDatabase) {
return;
}
const deviceId = await getDeviceToken(appDatabase);
if (!deviceId) {
return;
}
let client;
try {
client = NetworkManager.getClient(serverUrl);
} catch (err) {
return;
}
try {
const response = await client.ping(deviceId);
const canReceiveNotifications = response?.data?.CanReceiveNotifications;
switch (canReceiveNotifications) {
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_NOT_AVAILABLE}], prepareRecordsOnly: false});
return;
case PUSH_PROXY_RESPONSE_UNKNOWN:
return;
default:
operator.handleSystem({systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}], prepareRecordsOnly: false});
}
} catch (err) {
// Do nothing
}
}
export async function upgradeEntry(serverUrl: string) {
const dt = Date.now();
const operator = DatabaseManager.serverDatabases[serverUrl]?.operator;

View File

@@ -2,17 +2,40 @@
// See LICENSE.txt for license information.
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_RESPONSE_VERIFIED, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {getExpandedLinks} from '@queries/servers/system';
import {getDeviceToken} from '@queries/app/global';
import {getExpandedLinks, getPushVerificationStatus} from '@queries/servers/system';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type {ClientResponse} from '@mattermost/react-native-network-client';
export const doPing = async (serverUrl: string) => {
async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
if (!checkDeviceId) {
return undefined;
}
const serverDatabase = DatabaseManager.serverDatabases?.[serverUrl]?.database;
if (serverDatabase) {
const status = await getPushVerificationStatus(serverDatabase);
if (status === PUSH_PROXY_STATUS_VERIFIED) {
return undefined;
}
}
const appDatabase = DatabaseManager.appDatabase?.database;
if (!appDatabase) {
return '';
}
return getDeviceToken(appDatabase);
}
export const doPing = async (serverUrl: string, verifyPushProxy: boolean) => {
let client: Client;
try {
client = await NetworkManager.createClient(serverUrl);
@@ -30,9 +53,11 @@ export const doPing = async (serverUrl: string) => {
defaultMessage: 'Cannot connect to the server.',
};
const deviceId = await getDeviceIdForPing(serverUrl, verifyPushProxy);
let response: ClientResponse;
try {
response = await client.ping();
response = await client.ping(deviceId);
if (response.code === 401) {
// Don't invalidate the client since we want to eventually
@@ -50,6 +75,17 @@ export const doPing = async (serverUrl: string) => {
return {error: {intl: pingError}};
}
if (verifyPushProxy) {
let canReceiveNotifications = response?.data?.CanReceiveNotifications;
// Already verified or old server
if (deviceId === undefined || canReceiveNotifications === null) {
canReceiveNotifications = PUSH_PROXY_RESPONSE_VERIFIED;
}
return {canReceiveNotifications, error: undefined};
}
return {error: undefined};
};

View File

@@ -12,6 +12,7 @@ import NetworkManager from '@managers/network_manager';
import WebsocketManager from '@managers/websocket_manager';
import {getDeviceToken} from '@queries/app/global';
import {getCurrentUserId, getCommonSystemValues} from '@queries/servers/system';
import EphemeralStore from '@store/ephemeral_store';
import {getCSRFFromCookie} from '@utils/security';
import {loginEntry} from './entry';
@@ -49,16 +50,28 @@ export const completeLogin = async (serverUrl: string, user: UserProfile) => {
await DatabaseManager.setActiveServerDatabase(serverUrl);
const systems: IdValue[] = [];
// Set push proxy verification
const ppVerification = EphemeralStore.getPushProxyVerificationState(serverUrl);
if (ppVerification) {
systems.push({id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: ppVerification});
}
// Start websocket
const credentials = await getServerCredentials(serverUrl);
if (credentials?.token) {
WebsocketManager.createClient(serverUrl, credentials.token);
return operator.handleSystem({systems: [{
systems.push({
id: SYSTEM_IDENTIFIERS.WEBSOCKET,
value: 0,
}],
prepareRecordsOnly: false});
});
}
if (systems.length) {
operator.handleSystem({systems, prepareRecordsOnly: false});
}
return null;
};

View File

@@ -7,7 +7,7 @@ import ClientError from './error';
export interface ClientGeneralMix {
getOpenGraphMetadata: (url: string) => Promise<any>;
ping: () => Promise<any>;
ping: (deviceId?: string) => Promise<any>;
logClientError: (message: string, level?: string) => Promise<any>;
getClientConfigOld: () => Promise<ClientConfig>;
getClientLicenseOld: () => Promise<ClientLicense>;
@@ -25,9 +25,13 @@ const ClientGeneral = (superclass: any) => class extends superclass {
);
};
ping = async () => {
ping = async (deviceId?: string) => {
let url = `${this.urlVersion}/system/ping?time=${Date.now()}`;
if (deviceId) {
url = `${url}&device_id=${deviceId}`;
}
return this.doFetch(
`${this.urlVersion}/system/ping?time=${Date.now()}`,
url,
{method: 'get'},
false,
);

View File

@@ -54,6 +54,7 @@ export const SYSTEM_IDENTIFIERS = {
DATA_RETENTION_POLICIES: 'dataRetentionPolicies',
EXPANDED_LINKS: 'expandedLinks',
LICENSE: 'license',
PUSH_VERIFICATION_STATUS: 'pushVerificationStatus',
RECENT_CUSTOM_STATUS: 'recentCustomStatus',
RECENT_MENTIONS: 'recentMentions',
RECENT_REACTIONS: 'recentReactions',

View File

@@ -24,6 +24,7 @@ import Post from './post';
import PostDraft from './post_draft';
import Preferences from './preferences';
import Profile from './profile';
import PushProxy from './push_proxy';
import Screens from './screens';
import ServerErrors from './server_errors';
import SnackBar from './snack_bar';
@@ -56,6 +57,7 @@ export {
PostDraft,
Preferences,
Profile,
PushProxy,
Screens,
ServerErrors,
SnackBar,

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export const PUSH_PROXY_STATUS_VERIFIED = 'verified';
export const PUSH_PROXY_STATUS_UNKNOWN = 'unknown';
export const PUSH_PROXY_STATUS_NOT_AVAILABLE = 'not_available';
export const PUSH_PROXY_RESPONSE_VERIFIED = 'true';
export const PUSH_PROXY_RESPONSE_UNKNOWN = 'unknown';
export const PUSH_PROXY_RESPONSE_NOT_AVAILABLE = 'false';
export default {
PUSH_PROXY_STATUS_VERIFIED,
PUSH_PROXY_STATUS_UNKNOWN,
PUSH_PROXY_STATUS_NOT_AVAILABLE,
PUSH_PROXY_RESPONSE_VERIFIED,
PUSH_PROXY_RESPONSE_UNKNOWN,
PUSH_PROXY_RESPONSE_NOT_AVAILABLE,
};

View File

@@ -9,9 +9,9 @@ import type Global from '@typings/database/models/app/global';
const {APP: {GLOBAL}} = MM_TABLES;
export const getDeviceToken = async (appDatabase: Database) => {
export const getDeviceToken = async (appDatabase: Database): Promise<string> => {
try {
const tokens = await appDatabase.get(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN) as Global;
const tokens = await appDatabase.get<Global>(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN);
return tokens?.value || '';
} catch {
return '';

View File

@@ -6,6 +6,7 @@ import {of as of$} from 'rxjs';
import {switchMap} from 'rxjs/operators';
import {MM_TABLES, SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
import type ServerDataOperator from '@database/operator/server_data_operator';
import type SystemModel from '@typings/database/models/servers/system';
@@ -72,7 +73,22 @@ export const getCurrentUserId = async (serverDatabase: Database): Promise<string
export const observeCurrentUserId = (database: Database) => {
return querySystemValue(database, SYSTEM_IDENTIFIERS.CURRENT_USER_ID).observe().pipe(
switchMap((result) => (result.length ? result[0].observe() : of$({value: ''}))),
).pipe(
switchMap((model) => of$(model.value as string)),
);
};
export const getPushVerificationStatus = async (serverDatabase: Database): Promise<string> => {
try {
const status = await serverDatabase.get<SystemModel>(SYSTEM).find(SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS);
return status?.value || '';
} catch {
return '';
}
};
export const observePushVerificationStatus = (database: Database) => {
return querySystemValue(database, SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS).observe().pipe(
switchMap((result) => (result.length ? result[0].observe() : of$({value: PUSH_PROXY_STATUS_UNKNOWN}))),
switchMap((model) => of$(model.value as string)),
);
};

View File

@@ -148,23 +148,32 @@ exports[`components/categories_list should render channels error 1`] = `
/>
</View>
</View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
<View
style={
Object {
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 11,
"fontWeight": "600",
"lineHeight": 16,
"paddingRight": 30,
"alignItems": "center",
"flexDirection": "row",
"paddingRight": 60,
}
}
testID="channel_list_header.server_display_name"
>
</Text>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 11,
"fontWeight": "600",
"lineHeight": 16,
}
}
testID="channel_list_header.server_display_name"
>
</Text>
</View>
</View>
<View
style={
@@ -340,12 +349,17 @@ exports[`components/categories_list should render team error 1`] = `
>
<View
style={
Object {
"alignItems": "center",
"flexDirection": "row",
"height": 40,
"justifyContent": "space-between",
}
Array [
Object {
"alignItems": "center",
"flexDirection": "row",
"height": 40,
"justifyContent": "space-between",
},
Object {
"flex": 1,
},
]
}
>
<Text

View File

@@ -128,22 +128,31 @@ exports[`components/channel_list/header Channel List Header Component should mat
/>
</View>
</View>
<Text
ellipsizeMode="tail"
numberOfLines={1}
<View
style={
Object {
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 11,
"fontWeight": "600",
"lineHeight": 16,
"paddingRight": 30,
"alignItems": "center",
"flexDirection": "row",
"paddingRight": 60,
}
}
testID="channel_list_header.server_display_name"
>
</Text>
<Text
ellipsizeMode="tail"
numberOfLines={1}
style={
Object {
"color": "rgba(255,255,255,0.64)",
"fontFamily": "OpenSans-SemiBold",
"fontSize": 11,
"fontWeight": "600",
"lineHeight": 16,
}
}
testID="channel_list_header.server_display_name"
>
</Text>
</View>
</View>
`;

View File

@@ -3,6 +3,7 @@
import React from 'react';
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import {renderWithIntl} from '@test/intl-test-helper';
import Header from './header';
@@ -11,6 +12,7 @@ describe('components/channel_list/header', () => {
it('Channel List Header Component should match snapshot', () => {
const {toJSON} = renderWithIntl(
<Header
pushProxyStatus={PUSH_PROXY_STATUS_VERIFIED}
canCreateChannels={true}
canJoinChannels={true}
displayName={'Test!'}

View File

@@ -11,11 +11,13 @@ import {logout} from '@actions/remote/session';
import CompassIcon from '@components/compass_icon';
import {ITEM_HEIGHT} from '@components/slide_up_panel_item';
import TouchableWithFeedback from '@components/touchable_with_feedback';
import {PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import {useServerDisplayName, useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {bottomSheet} from '@screens/navigation';
import {bottomSheetSnapPoint} from '@utils/helpers';
import {alertPushProxyError, alertPushProxyUnknown} from '@utils/push_proxy';
import {alertServerLogout} from '@utils/server';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -28,6 +30,7 @@ type Props = {
displayName: string;
iconPad?: boolean;
onHeaderPress?: () => void;
pushProxyStatus: string;
}
const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
@@ -37,7 +40,6 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
},
subHeadingStyles: {
color: changeOpacity(theme.sidebarText, 0.64),
paddingRight: 30,
...typography('Heading', 50),
},
headerRow: {
@@ -64,6 +66,14 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
color: changeOpacity(theme.sidebarText, 0.8),
fontSize: 18,
},
pushAlert: {
marginLeft: 5,
},
subHeadingView: {
flexDirection: 'row',
alignItems: 'center',
paddingRight: 60,
},
noTeamHeadingStyles: {
color: changeOpacity(theme.sidebarText, 0.64),
...typography('Body', 100, 'SemiBold'),
@@ -78,7 +88,14 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({
const hitSlop: Insets = {top: 10, bottom: 30, left: 20, right: 20};
const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, iconPad, onHeaderPress}: Props) => {
const ChannelListHeader = ({
canCreateChannels,
canJoinChannels,
displayName,
iconPad,
onHeaderPress,
pushProxyStatus,
}: Props) => {
const theme = useTheme();
const isTablet = useIsTablet();
const intl = useIntl();
@@ -90,7 +107,6 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
marginLeft: withTiming(marginLeft.value, {duration: 350}),
}), []);
const serverUrl = useServerUrl();
useEffect(() => {
marginLeft.value = iconPad ? 44 : 0;
}, [iconPad]);
@@ -124,6 +140,14 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
});
}, [intl, insets, isTablet, theme]);
const onPushAlertPress = useCallback(() => {
if (pushProxyStatus === PUSH_PROXY_STATUS_NOT_AVAILABLE) {
alertPushProxyError(intl);
} else {
alertPushProxyUnknown(intl);
}
}, [pushProxyStatus, intl]);
const onLogoutPress = useCallback(() => {
alertServerLogout(serverDisplayName, () => logout(serverUrl), intl);
}, []);
@@ -168,20 +192,36 @@ const ChannelListHeader = ({canCreateChannels, canJoinChannels, displayName, ico
/>
</TouchableWithFeedback>
</View>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.subHeadingStyles}
testID='channel_list_header.server_display_name'
>
{serverDisplayName}
</Text>
<View style={styles.subHeadingView}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.subHeadingStyles}
testID='channel_list_header.server_display_name'
>
{serverDisplayName}
</Text>
{(pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED) && (
<TouchableWithFeedback
onPress={onPushAlertPress}
testID='channel_list_header.push_alert'
type='opacity'
>
<CompassIcon
name='alert-outline'
color={theme.errorTextColor}
size={14}
style={styles.pushAlert}
/>
</TouchableWithFeedback>
)}
</View>
</>
);
} else {
header = (
<View style={styles.noTeamHeaderRow}>
<View style={styles.noTeamHeaderRow}>
<View style={[styles.noTeamHeaderRow, {flex: 1}]}>
<Text
numberOfLines={1}
ellipsizeMode='tail'

View File

@@ -8,6 +8,7 @@ import {switchMap} from 'rxjs/operators';
import {Permissions} from '@constants';
import {observePermissionForTeam} from '@queries/servers/role';
import {observePushVerificationStatus} from '@queries/servers/system';
import {observeCurrentTeam} from '@queries/servers/team';
import {observeCurrentUser} from '@queries/servers/user';
@@ -42,6 +43,7 @@ const enhanced = withObservables([], ({database}: WithDatabaseArgs) => {
displayName: team.pipe(
switchMap((t) => of$(t?.displayName)),
),
pushProxyStatus: observePushVerificationStatus(database),
};
});

View File

@@ -6,6 +6,9 @@ import {of as of$} from 'rxjs';
import {catchError, switchMap} from 'rxjs/operators';
import {GLOBAL_IDENTIFIERS, MM_TABLES} from '@constants/database';
import {PUSH_PROXY_STATUS_UNKNOWN} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
import {observePushVerificationStatus} from '@queries/servers/system';
import ServerItem from './server_item';
@@ -24,9 +27,12 @@ const enhance = withObservables(['highlight'], ({highlight, server}: {highlight:
);
}
const serverDatabase = DatabaseManager.serverDatabases[server.url]?.database;
return {
server: server.observe(),
tutorialWatched,
pushProxyStatus: serverDatabase ? observePushVerificationStatus(serverDatabase) : of$(PUSH_PROXY_STATUS_UNKNOWN),
};
});

View File

@@ -18,11 +18,14 @@ import ServerIcon from '@components/server_icon';
import TutorialHighlight from '@components/tutorial_highlight';
import TutorialSwipeLeft from '@components/tutorial_highlight/swipe_left';
import {Events} from '@constants';
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import {useTheme} from '@context/theme';
import DatabaseManager from '@database/manager';
import {subscribeServerUnreadAndMentions, UnreadObserverArgs} from '@database/subscription/unreads';
import {useIsTablet} from '@hooks/device';
import {dismissBottomSheet} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {alertPushProxyError, alertPushProxyUnknown} from '@utils/push_proxy';
import {alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@@ -39,6 +42,7 @@ type Props = {
isActive: boolean;
server: ServersModel;
tutorialWatched: boolean;
pushProxyStatus: string;
}
type BadgeValues = {
@@ -86,6 +90,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
name: {
color: theme.centerChannelColor,
...typography('Body', 200, 'SemiBold'),
flex: 1,
},
offline: {
opacity: 0.5,
@@ -105,6 +110,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
url: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 75, 'Regular'),
marginRight: 7,
},
switching: {
height: 40,
@@ -117,9 +123,28 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
tutorialTablet: {
top: -80,
},
nameView: {
flexDirection: 'row',
marginRight: 7,
},
pushAlert: {
marginLeft: 7,
alignSelf: 'center',
},
pushAlertText: {
color: theme.errorTextColor,
...typography('Body', 75, 'Regular'),
marginBottom: 12,
},
}));
const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
const ServerItem = ({
highlight,
isActive,
server,
tutorialWatched,
pushProxyStatus,
}: Props) => {
const intl = useIntl();
const theme = useTheme();
const isTablet = useIsTablet();
@@ -201,12 +226,26 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
const handleLogin = useCallback(async () => {
swipeable.current?.close();
setSwitching(true);
const result = await doPing(server.url);
const result = await doPing(server.url, true);
if (result.error) {
alertServerError(intl, result.error as ClientErrorProps);
setSwitching(false);
return;
}
switch (result.canReceiveNotifications) {
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
EphemeralStore.setPushProxyVerificationState(server.url, PUSH_PROXY_STATUS_NOT_AVAILABLE);
alertPushProxyError(intl);
break;
case PUSH_PROXY_RESPONSE_UNKNOWN:
EphemeralStore.setPushProxyVerificationState(server.url, PUSH_PROXY_STATUS_UNKNOWN);
alertPushProxyUnknown(intl);
break;
default:
EphemeralStore.setPushProxyVerificationState(server.url, PUSH_PROXY_STATUS_VERIFIED);
}
const data = await fetchConfigAndLicense(server.url, true);
if (data.error) {
alertServerError(intl, data.error as ClientErrorProps);
@@ -311,6 +350,16 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
const serverItem = `server_list.server_item.${server.displayName.replace(/ /g, '_').toLocaleLowerCase()}`;
const serverItemTestId = isActive ? `${serverItem}.active` : `${serverItem}.inactive`;
const pushAlertText = pushProxyStatus === PUSH_PROXY_STATUS_NOT_AVAILABLE ?
intl.formatMessage({
id: 'server_list.push_proxy_error',
defaultMessage: 'Notifications cannot be received from this server because of its configuration. Contact your system admin.',
}) :
intl.formatMessage({
id: 'server_list.push_proxy_unknown',
defaultMessage: 'Notifications could not be received from this server because of its configuration. Log out and Log in again to retry.',
});
return (
<>
<Swipeable
@@ -355,13 +404,23 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
/>
}
<View style={styles.details}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.name}
>
{displayName}
</Text>
<View style={styles.nameView}>
<Text
numberOfLines={1}
ellipsizeMode='tail'
style={styles.name}
>
{displayName}
</Text>
{pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && (
<CompassIcon
name='alert-outline'
color={theme.errorTextColor}
size={14}
style={styles.pushAlert}
/>
)}
</View>
<Text
numberOfLines={1}
ellipsizeMode='tail'
@@ -383,6 +442,12 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => {
</RectButton>
</View>
</Swipeable>
{pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && (
<Text style={styles.pushAlertText}>
{pushAlertText}
</Text>
)}
{Boolean(database) && server.lastActiveAt > 0 &&
<WebSocket
database={database}

View File

@@ -24,10 +24,21 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
marginTop: 20,
marginHorizontal: 24,
},
text: {
logoutText: {
color: changeOpacity(theme.sidebarText, 0.64),
...typography('Body', 100, 'SemiBold'),
},
displayNameText: {
color: changeOpacity(theme.sidebarText, 0.64),
...typography('Body', 100, 'SemiBold'),
flex: 1,
},
logoutContainer: {
marginLeft: 10,
},
displayNameContainer: {
flex: 1,
},
}));
const MARGIN_WITH_SERVER_ICON = 66;
@@ -53,8 +64,9 @@ function Header() {
let serverLabel = (
<Text
style={styles.text}
style={styles.displayNameText}
testID='select_team.server_display_name'
numberOfLines={1}
>
{serverDisplayName}
</Text>
@@ -65,6 +77,7 @@ function Header() {
onPress={onLabelPress}
type='opacity'
testID='select_team.server_display_name.touchable'
style={styles.displayNameContainer}
>
{serverLabel}
</TouchableWithFeedback>
@@ -80,9 +93,10 @@ function Header() {
onPress={onLogoutPress}
testID='select_team.logout.button'
type='opacity'
style={styles.logoutContainer}
>
<Text
style={styles.text}
style={styles.logoutText}
testID='select_team.logout.text'
>
{intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})}

View File

@@ -16,14 +16,17 @@ import LocalConfig from '@assets/config.json';
import ClientError from '@client/rest/error';
import AppVersion from '@components/app_version';
import {Screens} from '@constants';
import {PUSH_PROXY_RESPONSE_NOT_AVAILABLE, PUSH_PROXY_RESPONSE_UNKNOWN, PUSH_PROXY_STATUS_NOT_AVAILABLE, PUSH_PROXY_STATUS_UNKNOWN, PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
import {t} from '@i18n';
import NetworkManager from '@managers/network_manager';
import {queryServerByDisplayName, queryServerByIdentifier} from '@queries/app/servers';
import Background from '@screens/background';
import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation';
import EphemeralStore from '@store/ephemeral_store';
import {DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch';
import {getErrorMessage} from '@utils/client_error';
import {alertPushProxyError, alertPushProxyUnknown} from '@utils/push_proxy';
import {loginOptions} from '@utils/server';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {getServerUrlAfterRedirect, isValidUrl, sanitizeUrl} from '@utils/url';
@@ -247,7 +250,7 @@ const Server = ({
};
const serverUrl = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp);
const result = await doPing(serverUrl);
const result = await doPing(serverUrl, true);
if (canceled) {
return;
@@ -265,6 +268,19 @@ const Server = ({
return;
}
switch (result.canReceiveNotifications) {
case PUSH_PROXY_RESPONSE_NOT_AVAILABLE:
EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_NOT_AVAILABLE);
alertPushProxyError(intl);
break;
case PUSH_PROXY_RESPONSE_UNKNOWN:
EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_UNKNOWN);
alertPushProxyUnknown(intl);
break;
default:
EphemeralStore.setPushProxyVerificationState(serverUrl, PUSH_PROXY_STATUS_VERIFIED);
}
const data = await fetchConfigAndLicense(serverUrl, true);
if (data.error) {
setButtonDisabled(true);

View File

@@ -10,6 +10,8 @@ class EphemeralStore {
creatingChannel = false;
creatingDMorGMTeammates: string[] = [];
private pushProxyVerification: {[x: string]: string | undefined} = {};
// As of today, the server sends a duplicated event to add the user to the team.
// If we do not handle this, this ends up showing some errors in the database, apart
// of the extra computation time. We use this to track the events that are being handled
@@ -146,6 +148,14 @@ class EphemeralStore {
isAddingToTeam = (teamId: string) => {
return this.addingTeam.has(teamId);
};
setPushProxyVerificationState = (serverUrl: string, state: string) => {
this.pushProxyVerification[serverUrl] = state;
};
getPushProxyVerificationState = (serverUrl: string) => {
return this.pushProxyVerification[serverUrl];
};
}
export default new EphemeralStore();

37
app/utils/push_proxy.ts Normal file
View File

@@ -0,0 +1,37 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IntlShape} from 'react-intl';
import {Alert} from 'react-native';
export function alertPushProxyError(intl: IntlShape) {
Alert.alert(
intl.formatMessage({
id: 'alert.push_proxy_error.title',
defaultMessage: 'Notifications cannot be received from this server',
}),
intl.formatMessage({
id: 'alert.push_proxy_error.description',
defaultMessage: 'Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.',
}),
[{
text: intl.formatMessage({id: 'alert.push_proxy.button', defaultMessage: 'Okay'}),
}],
);
}
export function alertPushProxyUnknown(intl: IntlShape) {
Alert.alert(
intl.formatMessage({
id: 'alert.push_proxy_unknown.title',
defaultMessage: 'Notifications could not be received from this server',
}),
intl.formatMessage({
id: 'alert.push_proxy_unknown.description',
defaultMessage: 'This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.',
}),
[{
text: intl.formatMessage({id: 'alert.push_proxy.button', defaultMessage: 'Okay'}),
}],
);
}

View File

@@ -15,6 +15,11 @@
"account.settings": "Settings",
"account.user_status.title": "User Presence",
"account.your_profile": "Your Profile",
"alert.push_proxy_error.description": "Due to the configuration for this server, notifications cannot be received in the mobile app. Contact your system admin for more information.",
"alert.push_proxy_error.title": "Notifications cannot be received from this server",
"alert.push_proxy_unknown.description": "This server was unable to receive push notifications for an unknown reason. This will be attempted again next time you connect.",
"alert.push_proxy_unknown.title": "Notifications could not be received from this server",
"alert.push_proxy.button": "Okay",
"alert.removed_from_team.description": "You have been removed from team {displayName}.",
"alert.removed_from_team.title": "Removed from team",
"api.channel.add_guest.added": "{addedUsername} added to the channel as a guest by {username}.",
@@ -531,6 +536,8 @@
"select_team.no_team.description": "To join a team, ask a team admin for an invite, or create your own team. You may also want to check your email inbox for an invitation.",
"select_team.no_team.title": "No teams are available to join",
"select_team.title": "Select a team",
"server_list.push_proxy_error": "Notifications cannot be received from this server because of its configuration. Contact your system admin.",
"server_list.push_proxy_unknown": "Notifications could not be received from this server because of its configuration. Log out and Log in again to retry.",
"server.logout.alert_description": "All associated data will be removed",
"server.logout.alert_title": "Are you sure you want to log out of {displayName}?",
"server.remove.alert_description": "This will remove it from your list of servers. All associated data will be removed",

View File

@@ -10,6 +10,8 @@ import nock from 'nock';
import Config from '@assets/config.json';
import {Client} from '@client/rest';
import {SYSTEM_IDENTIFIERS} from '@constants/database';
import {PUSH_PROXY_STATUS_VERIFIED} from '@constants/push_proxy';
import DatabaseManager from '@database/manager';
import {prepareCommonSystemValues} from '@queries/servers/system';
import {generateId} from '@utils/general';
@@ -106,6 +108,11 @@ class TestHelper {
await operator.batchRecords(systems);
}
await operator.handleSystem({
prepareRecordsOnly: false,
systems: [{id: SYSTEM_IDENTIFIERS.PUSH_VERIFICATION_STATUS, value: PUSH_PROXY_STATUS_VERIFIED}],
});
return {database, operator};
};