From 7e8084309223d73a6455aa39221b631e62f3afb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Mon, 9 May 2022 11:41:16 +0200 Subject: [PATCH] [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 --- app/actions/remote/entry/app.ts | 55 +++++++++++- app/actions/remote/general.ts | 42 +++++++++- app/actions/remote/session.ts | 19 ++++- app/client/rest/general.ts | 10 ++- app/constants/database.ts | 1 + app/constants/index.ts | 2 + app/constants/push_proxy.ts | 19 +++++ app/queries/app/global.ts | 4 +- app/queries/servers/system.ts | 18 +++- .../__snapshots__/index.test.tsx.snap | 50 +++++++---- .../header/__snapshots__/header.test.tsx.snap | 33 +++++--- .../categories_list/header/header.test.tsx | 2 + .../categories_list/header/header.tsx | 64 +++++++++++--- .../categories_list/header/index.ts | 2 + .../servers/servers_list/server_item/index.ts | 6 ++ .../servers_list/server_item/server_item.tsx | 83 +++++++++++++++++-- app/screens/select_team/header.tsx | 20 ++++- app/screens/server/index.tsx | 18 +++- app/store/ephemeral_store.ts | 10 +++ app/utils/push_proxy.ts | 37 +++++++++ assets/base/i18n/en.json | 7 ++ test/test_helper.ts | 7 ++ 22 files changed, 441 insertions(+), 68 deletions(-) create mode 100644 app/constants/push_proxy.ts create mode 100644 app/utils/push_proxy.ts diff --git a/app/actions/remote/entry/app.ts b/app/actions/remote/entry/app.ts index f5caae553..bbec44e31 100644 --- a/app/actions/remote/entry/app.ts +++ b/app/actions/remote/entry/app.ts @@ -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; diff --git a/app/actions/remote/general.ts b/app/actions/remote/general.ts index d9acf9be7..ba9c6eca2 100644 --- a/app/actions/remote/general.ts +++ b/app/actions/remote/general.ts @@ -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}; }; diff --git a/app/actions/remote/session.ts b/app/actions/remote/session.ts index 0f9c8ff07..2aff72615 100644 --- a/app/actions/remote/session.ts +++ b/app/actions/remote/session.ts @@ -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; }; diff --git a/app/client/rest/general.ts b/app/client/rest/general.ts index ecff9d770..ed44a159f 100644 --- a/app/client/rest/general.ts +++ b/app/client/rest/general.ts @@ -7,7 +7,7 @@ import ClientError from './error'; export interface ClientGeneralMix { getOpenGraphMetadata: (url: string) => Promise; - ping: () => Promise; + ping: (deviceId?: string) => Promise; logClientError: (message: string, level?: string) => Promise; getClientConfigOld: () => Promise; getClientLicenseOld: () => Promise; @@ -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, ); diff --git a/app/constants/database.ts b/app/constants/database.ts index b3836db41..0fdca2fd4 100644 --- a/app/constants/database.ts +++ b/app/constants/database.ts @@ -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', diff --git a/app/constants/index.ts b/app/constants/index.ts index 3fc3edcf5..0f6c9a200 100644 --- a/app/constants/index.ts +++ b/app/constants/index.ts @@ -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, diff --git a/app/constants/push_proxy.ts b/app/constants/push_proxy.ts new file mode 100644 index 000000000..835c2204c --- /dev/null +++ b/app/constants/push_proxy.ts @@ -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, +}; diff --git a/app/queries/app/global.ts b/app/queries/app/global.ts index cb514e890..ecc212736 100644 --- a/app/queries/app/global.ts +++ b/app/queries/app/global.ts @@ -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 => { try { - const tokens = await appDatabase.get(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN) as Global; + const tokens = await appDatabase.get(GLOBAL).find(GLOBAL_IDENTIFIERS.DEVICE_TOKEN); return tokens?.value || ''; } catch { return ''; diff --git a/app/queries/servers/system.ts b/app/queries/servers/system.ts index 7cbca90a5..a934b0770 100644 --- a/app/queries/servers/system.ts +++ b/app/queries/servers/system.ts @@ -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 { 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 => { + try { + const status = await serverDatabase.get(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)), ); }; diff --git a/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap b/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap index 2fb63e9c8..5c8055160 100644 --- a/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap +++ b/app/screens/home/channel_list/categories_list/__snapshots__/index.test.tsx.snap @@ -148,23 +148,32 @@ exports[`components/categories_list should render channels error 1`] = ` /> - - - + + + + - - - + + + + `; diff --git a/app/screens/home/channel_list/categories_list/header/header.test.tsx b/app/screens/home/channel_list/categories_list/header/header.test.tsx index a9725eb2d..2b425144e 100644 --- a/app/screens/home/channel_list/categories_list/header/header.test.tsx +++ b/app/screens/home/channel_list/categories_list/header/header.test.tsx @@ -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(
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 /> - - {serverDisplayName} - + + + {serverDisplayName} + + {(pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED) && ( + + + + )} + ); } else { header = ( - + { displayName: team.pipe( switchMap((t) => of$(t?.displayName)), ), + pushProxyStatus: observePushVerificationStatus(database), }; }); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts index 3c09e0d80..9332d2d97 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/index.ts +++ b/app/screens/home/channel_list/servers/servers_list/server_item/index.ts @@ -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), }; }); diff --git a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx index 7a435d36f..aa4eba3c6 100644 --- a/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx +++ b/app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx @@ -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 ( <> { /> } - - {displayName} - + + + {displayName} + + {pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && ( + + )} + { + {pushProxyStatus !== PUSH_PROXY_STATUS_VERIFIED && ( + + {pushAlertText} + + )} + {Boolean(database) && server.lastActiveAt > 0 && ({ 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 = ( {serverDisplayName} @@ -65,6 +77,7 @@ function Header() { onPress={onLabelPress} type='opacity' testID='select_team.server_display_name.touchable' + style={styles.displayNameContainer} > {serverLabel} @@ -80,9 +93,10 @@ function Header() { onPress={onLogoutPress} testID='select_team.logout.button' type='opacity' + style={styles.logoutContainer} > {intl.formatMessage({id: 'account.logout', defaultMessage: 'Log out'})} diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx index c0308a4a0..1c773c5ed 100644 --- a/app/screens/server/index.tsx +++ b/app/screens/server/index.tsx @@ -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); diff --git a/app/store/ephemeral_store.ts b/app/store/ephemeral_store.ts index b26b3a777..5faff2c21 100644 --- a/app/store/ephemeral_store.ts +++ b/app/store/ephemeral_store.ts @@ -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(); diff --git a/app/utils/push_proxy.ts b/app/utils/push_proxy.ts new file mode 100644 index 000000000..6c9707fa4 --- /dev/null +++ b/app/utils/push_proxy.ts @@ -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'}), + }], + ); +} diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json index 65ff9d5de..5500ba799 100644 --- a/assets/base/i18n/en.json +++ b/assets/base/i18n/en.json @@ -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", diff --git a/test/test_helper.ts b/test/test_helper.ts index 864c6dc0a..180a798a4 100644 --- a/test/test_helper.ts +++ b/test/test_helper.ts @@ -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}; };