From bb3a3112b19ed953bbcfd55bf98282cd67f1438d Mon Sep 17 00:00:00 2001 From: Elias Nahum Date: Mon, 25 Apr 2022 13:26:31 -0400 Subject: [PATCH] [Gekidou] Show login screen when selecting a previous server (#6190) * Show login screen when selecting a previous server * Update app/screens/home/channel_list/servers/servers_list/server_item/server_item.tsx Co-authored-by: Jason Frerich Co-authored-by: Jason Frerich --- .../servers_list/server_item/server_item.tsx | 22 +++++- app/screens/login/index.tsx | 22 +++++- app/screens/navigation.ts | 2 + app/screens/server/index.tsx | 23 ++----- app/screens/sso/index.tsx | 34 +++++++-- app/screens/sso/sso.test.tsx | 1 + app/utils/server/index.ts | 69 ++++++++++++++++++- test/setup.ts | 3 + 8 files changed, 144 insertions(+), 32 deletions(-) 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 913b477494..eb791a92d2 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 @@ -9,7 +9,9 @@ import Swipeable from 'react-native-gesture-handler/Swipeable'; import {storeMultiServerTutorial} from '@actions/app/global'; import {appEntry} from '@actions/remote/entry'; +import {doPing} from '@actions/remote/general'; import {logout} from '@actions/remote/session'; +import {fetchConfigAndLicense} from '@actions/remote/systems'; import CompassIcon from '@components/compass_icon'; import Loading from '@components/loading'; import ServerIcon from '@components/server_icon'; @@ -21,7 +23,7 @@ import DatabaseManager from '@database/manager'; import {subscribeServerUnreadAndMentions} from '@database/subscription/unreads'; import {useIsTablet} from '@hooks/device'; import {dismissBottomSheet} from '@screens/navigation'; -import {addNewServer, alertServerLogout, alertServerRemove, editServer} from '@utils/server'; +import {alertServerError, alertServerLogout, alertServerRemove, editServer, loginToServer} from '@utils/server'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {removeProtocol, stripTrailingSlashes} from '@utils/url'; @@ -195,8 +197,22 @@ const ServerItem = ({highlight, isActive, server, tutorialWatched}: Props) => { return style; }, [server.lastActiveAt]); - const handleLogin = useCallback(() => { - addNewServer(theme, server.url, displayName); + const handleLogin = useCallback(async () => { + swipeable.current?.close(); + setSwitching(true); + const result = await doPing(server.url); + if (result.error) { + alertServerError(intl, result.error as ClientErrorProps); + setSwitching(false); + return; + } + const data = await fetchConfigAndLicense(server.url, true); + if (data.error) { + alertServerError(intl, data.error as ClientErrorProps); + setSwitching(false); + return; + } + loginToServer(theme, server.url, displayName, data.config!, data.license!); }, [server, theme, intl]); const handleDismissTutorial = useCallback(() => { diff --git a/app/screens/login/index.tsx b/app/screens/login/index.tsx index 3277492a19..3a93540b52 100644 --- a/app/screens/login/index.tsx +++ b/app/screens/login/index.tsx @@ -11,8 +11,9 @@ import {SafeAreaView} from 'react-native-safe-area-context'; import FormattedText from '@components/formatted_text'; import {Screens} from '@constants'; import {useIsTablet} from '@hooks/device'; +import NetworkManager from '@managers/network_manager'; import Background from '@screens/background'; -import {goToScreen, loginAnimationOptions} from '@screens/navigation'; +import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation'; import {preventDoubleTap} from '@utils/tap'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -24,6 +25,8 @@ import SsoOptions from './sso_options'; import type {LaunchProps} from '@typings/launch'; export interface LoginOptionsProps extends LaunchProps { + closeButtonId?: string; + componentId: string; config: ClientConfig; hasLoginForm: boolean; license: ClientLicense; @@ -68,7 +71,11 @@ const getStyles = makeStyleSheetFromTheme((theme: Theme) => ({ const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); -const LoginOptions = ({config, extra, hasLoginForm, launchType, launchError, license, serverDisplayName, serverUrl, ssoOptions, theme}: LoginOptionsProps) => { +const LoginOptions = ({ + closeButtonId, componentId, config, extra, + hasLoginForm, launchType, launchError, license, + serverDisplayName, serverUrl, ssoOptions, theme, +}: LoginOptionsProps) => { const styles = getStyles(theme); const keyboardAwareRef = useRef(); const dimensions = useWindowDimensions(); @@ -125,6 +132,17 @@ const LoginOptions = ({config, extra, hasLoginForm, launchType, launchError, lic }; }, []); + useEffect(() => { + const navigationEvents = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => { + if (closeButtonId && buttonId === closeButtonId) { + NetworkManager.invalidateClient(serverUrl); + dismissModal({componentId}); + } + }); + + return () => navigationEvents.remove(); + }, []); + useEffect(() => { const listener = { componentDidAppear: () => { diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts index 969393e23d..0b7a8e03d5 100644 --- a/app/screens/navigation.ts +++ b/app/screens/navigation.ts @@ -184,6 +184,8 @@ export function resetToHome(passProps: LaunchProps = {launchType: LaunchType.Nor if (passProps.launchType === LaunchType.AddServer) { dismissModal({componentId: Screens.SERVER}); + dismissModal({componentId: Screens.LOGIN}); + dismissModal({componentId: Screens.SSO}); dismissModal({componentId: Screens.BOTTOM_SHEET}); return; } diff --git a/app/screens/server/index.tsx b/app/screens/server/index.tsx index a149744096..c0308a4a05 100644 --- a/app/screens/server/index.tsx +++ b/app/screens/server/index.tsx @@ -15,7 +15,7 @@ import {fetchConfigAndLicense} from '@actions/remote/systems'; import LocalConfig from '@assets/config.json'; import ClientError from '@client/rest/error'; import AppVersion from '@components/app_version'; -import {Screens, Sso} from '@constants'; +import {Screens} from '@constants'; import DatabaseManager from '@database/manager'; import {t} from '@i18n'; import NetworkManager from '@managers/network_manager'; @@ -24,6 +24,7 @@ import Background from '@screens/background'; import {dismissModal, goToScreen, loginAnimationOptions} from '@screens/navigation'; import {DeepLinkWithData, LaunchProps, LaunchType} from '@typings/launch'; import {getErrorMessage} from '@utils/client_error'; +import {loginOptions} from '@utils/server'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {getServerUrlAfterRedirect, isValidUrl, sanitizeUrl} from '@utils/url'; @@ -136,6 +137,7 @@ const Server = ({ useEffect(() => { const navigationEvents = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => { if (closeButtonId && buttonId === closeButtonId) { + NetworkManager.invalidateClient(url); dismissModal({componentId}); } }); @@ -144,24 +146,7 @@ const Server = ({ }, []); const displayLogin = (serverUrl: string, config: ClientConfig, license: ClientLicense) => { - const isLicensed = license.IsLicensed === 'true'; - const samlEnabled = config.EnableSaml === 'true' && isLicensed && license.SAML === 'true'; - const gitlabEnabled = config.EnableSignUpWithGitLab === 'true'; - const googleEnabled = config.EnableSignUpWithGoogle === 'true' && isLicensed; - const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && isLicensed && license.Office365OAuth === 'true'; - const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && isLicensed; - const ldapEnabled = isLicensed && config.EnableLdap === 'true' && license.LDAP === 'true'; - const hasLoginForm = config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true' || ldapEnabled; - const ssoOptions: Record = { - [Sso.SAML]: samlEnabled, - [Sso.GITLAB]: gitlabEnabled, - [Sso.GOOGLE]: googleEnabled, - [Sso.OFFICE365]: o365Enabled, - [Sso.OPENID]: openIdEnabled, - }; - const enabledSSOs = Object.keys(ssoOptions).filter((key) => ssoOptions[key]); - const numberSSOs = enabledSSOs.length; - + const {enabledSSOs, hasLoginForm, numberSSOs, ssoOptions} = loginOptions(config, license); const passProps = { config, extra, diff --git a/app/screens/sso/index.tsx b/app/screens/sso/index.tsx index 1fb9672c8e..f46cf9b12b 100644 --- a/app/screens/sso/index.tsx +++ b/app/screens/sso/index.tsx @@ -11,8 +11,9 @@ import {SafeAreaView} from 'react-native-safe-area-context'; import {ssoLogin} from '@actions/remote/session'; import ClientError from '@client/rest/error'; import {Screens, Sso} from '@constants'; +import NetworkManager from '@managers/network_manager'; import Background from '@screens/background'; -import {resetToHome} from '@screens/navigation'; +import {dismissModal, resetToHome} from '@screens/navigation'; import SSOWithRedirectURL from './sso_with_redirect_url'; import SSOWithWebView from './sso_with_webview'; @@ -20,11 +21,13 @@ import SSOWithWebView from './sso_with_webview'; import type {LaunchProps} from '@typings/launch'; interface SSOProps extends LaunchProps { - config: Partial; - license: Partial; - ssoType: string; - serverDisplayName: string; - theme: Theme; + closeButtonId?: string; + componentId: string; + config: Partial; + license: Partial; + ssoType: string; + serverDisplayName: string; + theme: Theme; } const AnimatedSafeArea = Animated.createAnimatedComponent(SafeAreaView); @@ -35,7 +38,11 @@ const styles = StyleSheet.create({ }, }); -const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverUrl, ssoType, theme}: SSOProps) => { +const SSO = ({ + closeButtonId, componentId, config, extra, + launchError, launchType, serverDisplayName, + serverUrl, ssoType, theme, +}: SSOProps) => { const managedConfig = useManagedConfig(); const inAppSessionAuth = managedConfig?.inAppSessionAuth === 'true'; const dimensions = useWindowDimensions(); @@ -128,6 +135,19 @@ const SSO = ({config, extra, launchError, launchType, serverDisplayName, serverU return () => unsubscribe.remove(); }, [dimensions]); + useEffect(() => { + const navigationEvents = Navigation.events().registerNavigationButtonPressedListener(({buttonId}) => { + if (closeButtonId && buttonId === closeButtonId) { + if (serverUrl) { + NetworkManager.invalidateClient(serverUrl); + } + dismissModal({componentId}); + } + }); + + return () => navigationEvents.remove(); + }, []); + const props = { doSSOLogin, loginError, diff --git a/app/screens/sso/sso.test.tsx b/app/screens/sso/sso.test.tsx index 45d37d3c26..741ac14ba8 100644 --- a/app/screens/sso/sso.test.tsx +++ b/app/screens/sso/sso.test.tsx @@ -23,6 +23,7 @@ jest.mock('@utils/url', () => { describe('SSO', () => { const baseProps = { + componentId: 'SSO', license: { IsLicensed: 'true', }, diff --git a/app/utils/server/index.ts b/app/utils/server/index.ts index 7417f1a962..4a0c86a4c1 100644 --- a/app/utils/server/index.ts +++ b/app/utils/server/index.ts @@ -5,9 +5,10 @@ import {IntlShape} from 'react-intl'; import {Alert, AlertButton} from 'react-native'; import CompassIcon from '@components/compass_icon'; -import {Screens, SupportedServer} from '@constants'; +import {Screens, Sso, SupportedServer} from '@constants'; import {dismissBottomSheet, showModal} from '@screens/navigation'; import {LaunchType} from '@typings/launch'; +import {getErrorMessage} from '@utils/client_error'; import {changeOpacity} from '@utils/theme'; import {tryOpenURL} from '@utils/url'; @@ -49,6 +50,61 @@ export async function addNewServer(theme: Theme, serverUrl?: string, displayName showModal(Screens.SERVER, '', props, options); } +export function loginOptions(config: ClientConfig, license: ClientLicense) { + const isLicensed = license.IsLicensed === 'true'; + const samlEnabled = config.EnableSaml === 'true' && isLicensed && license.SAML === 'true'; + const gitlabEnabled = config.EnableSignUpWithGitLab === 'true'; + const googleEnabled = config.EnableSignUpWithGoogle === 'true' && isLicensed; + const o365Enabled = config.EnableSignUpWithOffice365 === 'true' && isLicensed && license.Office365OAuth === 'true'; + const openIdEnabled = config.EnableSignUpWithOpenId === 'true' && isLicensed; + const ldapEnabled = isLicensed && config.EnableLdap === 'true' && license.LDAP === 'true'; + const hasLoginForm = config.EnableSignInWithEmail === 'true' || config.EnableSignInWithUsername === 'true' || ldapEnabled; + const ssoOptions: Record = { + [Sso.SAML]: samlEnabled, + [Sso.GITLAB]: gitlabEnabled, + [Sso.GOOGLE]: googleEnabled, + [Sso.OFFICE365]: o365Enabled, + [Sso.OPENID]: openIdEnabled, + }; + const enabledSSOs = Object.keys(ssoOptions).filter((key) => ssoOptions[key]); + const numberSSOs = enabledSSOs.length; + + return { + enabledSSOs, + hasLoginForm, + numberSSOs, + ssoOptions, + }; +} + +export async function loginToServer(theme: Theme, serverUrl: string, displayName: string, config: ClientConfig, license: ClientLicense) { + await dismissBottomSheet(); + const closeButtonId = 'close-server'; + const {enabledSSOs, hasLoginForm, numberSSOs, ssoOptions} = loginOptions(config, license); + const props = { + closeButtonId, + config, + hasLoginForm, + launchType: LaunchType.AddServer, + license, + serverDisplayName: displayName, + serverUrl, + ssoOptions, + theme, + }; + + const redirectSSO = !hasLoginForm && numberSSOs === 1; + const screen = redirectSSO ? Screens.SSO : Screens.LOGIN; + if (redirectSSO) { + // @ts-expect-error ssoType not in definition + props.ssoType = enabledSSOs[0]; + } + + const options = buildServerModalOptions(theme, closeButtonId); + + showModal(screen, '', props, options); +} + export async function editServer(theme: Theme, server: ServersModel) { const closeButtonId = 'close-server-edit'; const props = { @@ -103,6 +159,17 @@ export async function alertServerRemove(displayName: string, onPress: () => void ); } +export function alertServerError(intl: IntlShape, error: ClientErrorProps) { + const message = getErrorMessage(error, intl); + Alert.alert( + intl.formatMessage({ + id: 'server.websocket.unreachable', + defaultMessage: 'Server is unreachable.', + }), + message, + ); +} + function unsupportedServerAdminAlert(intl: IntlShape) { const title = intl.formatMessage({id: 'mobile.server_upgrade.title', defaultMessage: 'Server upgrade required'}); diff --git a/test/setup.ts b/test/setup.ts index 328a1d2d4c..d0e714b728 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -270,6 +270,9 @@ jest.mock('react-native-navigation', () => { bindComponent: jest.fn(() => { return {remove: jest.fn()}; }), + registerNavigationButtonPressedListener: jest.fn(() => { + return {buttonId: 'buttonId'}; + }), }), setRoot: jest.fn(), pop: jest.fn(),