open an external browser when the server config for MobileExternalBrowse is set to true (#8220) (#8223)

(cherry picked from commit f04838d9b1)

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Mattermost Build 2024-09-17 14:09:01 +02:00 committed by GitHub
parent 19009904a9
commit d91023d88e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 395 additions and 99 deletions

View file

@ -0,0 +1,75 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import React from 'react';
import {Text, View} from 'react-native';
import FormattedText from '@components/formatted_text';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
interface AuthErrorProps {
error: string;
retry: () => void;
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
button: {
marginTop: 25,
},
errorText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
textAlign: 'center',
...typography('Body', 200, 'Regular'),
},
infoContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
infoText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 100, 'Regular'),
},
infoTitle: {
color: theme.centerChannelColor,
marginBottom: 4,
...typography('Heading', 700),
},
};
});
const AuthError = ({error, retry, theme}: AuthErrorProps) => {
const style = getStyleSheet(theme);
return (
<View style={style.infoContainer}>
<FormattedText
id='mobile.oauth.switch_to_browser.error_title'
testID='mobile.oauth.switch_to_browser.error_title'
defaultMessage='Sign in error'
style={style.infoTitle}
/>
<Text style={style.errorText}>
{`${error}.`}
</Text>
<Button
buttonStyle={[style.button, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
testID='mobile.oauth.try_again'
onPress={retry}
>
<FormattedText
id='mobile.oauth.try_again'
defaultMessage='Try again'
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
/>
</Button>
</View>
);
};
export default AuthError;

View file

@ -0,0 +1,55 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import FormattedText from '@components/formatted_text';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
interface AuthRedirectProps {
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
infoContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
infoText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 100, 'Regular'),
},
infoTitle: {
color: theme.centerChannelColor,
marginBottom: 4,
...typography('Heading', 700),
},
};
});
const AuthRedirect = ({theme}: AuthRedirectProps) => {
const style = getStyleSheet(theme);
return (
<View style={style.infoContainer}>
<FormattedText
id='mobile.oauth.switch_to_browser.title'
testID='mobile.oauth.switch_to_browser.title'
defaultMessage='Redirecting...'
style={style.infoTitle}
/>
<FormattedText
id='mobile.oauth.switch_to_browser'
testID='mobile.oauth.switch_to_browser'
defaultMessage='You are being redirected to your login provider'
style={style.infoText}
/>
</View>
);
};
export default AuthRedirect;

View file

@ -0,0 +1,57 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React from 'react';
import {View} from 'react-native';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
interface AuthSuccessProps {
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
infoContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
infoText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 100, 'Regular'),
},
infoTitle: {
color: theme.centerChannelColor,
marginBottom: 4,
...typography('Heading', 700),
},
};
});
const AuthSuccess = ({theme}: AuthSuccessProps) => {
const style = getStyleSheet(theme);
return (
<View style={style.infoContainer}>
<Loading/>
<FormattedText
id='mobile.oauth.success.title'
testID='mobile.oauth.success.title'
defaultMessage='Authentication successful'
style={style.infoTitle}
/>
<FormattedText
id='mobile.oauth.success.description'
testID='mobile.oauth.success.description'
defaultMessage='Signing in now, just a moment...'
style={style.infoText}
/>
</View>
);
};
export default AuthSuccess;

View file

@ -18,6 +18,7 @@ import {getFullErrorMessage, isErrorWithUrl} from '@utils/errors';
import {logWarning} from '@utils/log';
import SSOAuthentication from './sso_authentication';
import SSOAuthenticationWithExternalBrowser from './sso_authentication_with_external_browser';
import type {LaunchProps} from '@typings/launch';
import type {AvailableScreens} from '@typings/screens/navigation';
@ -155,14 +156,28 @@ const SSO = ({
theme,
};
let authentication;
if (config.MobileExternalBrowser === 'true') {
authentication = (
<SSOAuthenticationWithExternalBrowser
{...props}
serverUrl={serverUrl!}
/>
);
} else {
authentication = (
<SSOAuthentication
{...props}
serverUrl={serverUrl!}
/>
);
}
return (
<View style={styles.flex}>
<Background theme={theme}/>
<AnimatedSafeArea style={[styles.flex, transform]}>
<SSOAuthentication
{...props}
serverUrl={serverUrl!}
/>
{authentication}
</AnimatedSafeArea>
</View>
);

View file

@ -1,24 +1,22 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Button} from '@rneui/base';
import {openAuthSessionAsync} from 'expo-web-browser';
import qs from 'querystringify';
import React, {useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {Linking, Platform, Text, View, type EventSubscription} from 'react-native';
import {Linking, Platform, StyleSheet, View, type EventSubscription} from 'react-native';
import urlParse from 'url-parse';
import FormattedText from '@components/formatted_text';
import Loading from '@components/loading';
import {Sso} from '@constants';
import NetworkManager from '@managers/network_manager';
import {buttonBackgroundStyle, buttonTextStyle} from '@utils/buttonStyles';
import {isBetaApp} from '@utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
interface SSOWithRedirectURLProps {
import AuthError from './components/auth_error';
import AuthRedirect from './components/auth_redirect';
import AuthSuccess from './components/auth_success';
interface SSOAuthenticationProps {
doSSOLogin: (bearerToken: string, csrfToken: string) => void;
loginError: string;
loginUrl: string;
@ -27,41 +25,16 @@ interface SSOWithRedirectURLProps {
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
button: {
marginTop: 25,
},
container: {
flex: 1,
paddingHorizontal: 24,
},
errorText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
textAlign: 'center',
...typography('Body', 200, 'Regular'),
},
infoContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
infoText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 100, 'Regular'),
},
infoTitle: {
color: theme.centerChannelColor,
marginBottom: 4,
...typography('Heading', 700),
},
};
const style = StyleSheet.create({
container: {
flex: 1,
paddingHorizontal: 24,
},
});
const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOWithRedirectURLProps) => {
const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOAuthenticationProps) => {
const [error, setError] = useState<string>('');
const [loginSuccess, setLoginSuccess] = useState(false);
const style = getStyleSheet(theme);
const intl = useIntl();
let customUrlScheme = Sso.REDIRECT_URL_SCHEME;
if (isBetaApp) {
@ -84,7 +57,7 @@ const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLogi
};
parsedUrl.set('query', qs.stringify(query));
const url = parsedUrl.toString();
const result = await openAuthSessionAsync(url, null, {preferEphemeralSession: true});
const result = await openAuthSessionAsync(url, null, {preferEphemeralSession: true, createTask: false});
if ('url' in result && result.url) {
const resultUrl = urlParse(result.url, true);
const bearerToken = resultUrl.query?.MMAUTHTOKEN;
@ -142,65 +115,17 @@ const SSOAuthentication = ({doSSOLogin, loginError, loginUrl, serverUrl, setLogi
let content;
if (loginSuccess) {
content = (
<View style={style.infoContainer}>
<Loading/>
<FormattedText
id='mobile.oauth.success.title'
testID='mobile.oauth.success.title'
defaultMessage='Authentication successful'
style={style.infoTitle}
/>
<FormattedText
id='mobile.oauth.success.description'
testID='mobile.oauth.success.description'
defaultMessage='Signing in now, just a moment...'
style={style.infoText}
/>
</View>
);
content = (<AuthSuccess theme={theme}/>);
} else if (loginError || error) {
content = (
<View style={style.infoContainer}>
<FormattedText
id='mobile.oauth.switch_to_browser.error_title'
testID='mobile.oauth.switch_to_browser.error_title'
defaultMessage='Sign in error'
style={style.infoTitle}
/>
<Text style={style.errorText}>
{`${loginError || error}.`}
</Text>
<Button
buttonStyle={[style.button, buttonBackgroundStyle(theme, 'lg', 'primary', 'default')]}
testID='mobile.oauth.try_again'
onPress={() => init()}
>
<FormattedText
id='mobile.oauth.try_again'
defaultMessage='Try again'
style={buttonTextStyle(theme, 'lg', 'primary', 'default')}
/>
</Button>
</View>
<AuthError
error={loginError || error}
retry={init}
theme={theme}
/>
);
} else {
content = (
<View style={style.infoContainer}>
<FormattedText
id='mobile.oauth.switch_to_browser.title'
testID='mobile.oauth.switch_to_browser.title'
defaultMessage='Redirecting...'
style={style.infoTitle}
/>
<FormattedText
id='mobile.oauth.switch_to_browser'
testID='mobile.oauth.switch_to_browser'
defaultMessage='You are being redirected to your login provider'
style={style.infoText}
/>
</View>
);
content = (<AuthRedirect theme={theme}/>);
}
return (

View file

@ -0,0 +1,166 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import qs from 'querystringify';
import React, {useEffect, useState} from 'react';
import {useIntl} from 'react-intl';
import {Linking, Platform, View} from 'react-native';
import urlParse from 'url-parse';
import {Sso} from '@constants';
import NetworkManager from '@managers/network_manager';
import {isErrorWithMessage} from '@utils/errors';
import {isBetaApp} from '@utils/general';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {tryOpenURL} from '@utils/url';
import AuthError from './components/auth_error';
import AuthRedirect from './components/auth_redirect';
import AuthSuccess from './components/auth_success';
interface SSOWithRedirectURLProps {
doSSOLogin: (bearerToken: string, csrfToken: string) => void;
loginError: string;
loginUrl: string;
serverUrl: string;
setLoginError: (value: string) => void;
theme: Theme;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
return {
button: {
marginTop: 25,
},
container: {
flex: 1,
paddingHorizontal: 24,
},
errorText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
textAlign: 'center',
...typography('Body', 200, 'Regular'),
},
infoContainer: {
alignItems: 'center',
flex: 1,
justifyContent: 'center',
},
infoText: {
color: changeOpacity(theme.centerChannelColor, 0.72),
...typography('Body', 100, 'Regular'),
},
infoTitle: {
color: theme.centerChannelColor,
marginBottom: 4,
...typography('Heading', 700),
},
};
});
const SSOAuthenticationWithExternalBrowser = ({doSSOLogin, loginError, loginUrl, serverUrl, setLoginError, theme}: SSOWithRedirectURLProps) => {
const [error, setError] = useState<string>('');
const [loginSuccess, setLoginSuccess] = useState(false);
const style = getStyleSheet(theme);
const intl = useIntl();
let customUrlScheme = Sso.REDIRECT_URL_SCHEME;
if (isBetaApp) {
customUrlScheme = Sso.REDIRECT_URL_SCHEME_DEV;
}
const redirectUrl = customUrlScheme + 'callback';
const init = (resetErrors = true) => {
setLoginSuccess(false);
if (resetErrors !== false) {
setError('');
setLoginError('');
NetworkManager.invalidateClient(serverUrl);
NetworkManager.createClient(serverUrl);
}
const parsedUrl = urlParse(loginUrl, true);
const query: Record<string, string> = {
...parsedUrl.query,
redirect_to: redirectUrl,
};
parsedUrl.set('query', qs.stringify(query));
const url = parsedUrl.toString();
const onError = (e: Error) => {
let message;
if (e && Platform.OS === 'android' && isErrorWithMessage(e) && e.message.match(/no activity found to handle intent/i)) {
message = intl.formatMessage({
id: 'mobile.oauth.failed_to_open_link_no_browser',
defaultMessage: 'The link failed to open. Please verify that a browser is installed on the device.',
});
} else {
message = intl.formatMessage({
id: 'mobile.oauth.failed_to_open_link',
defaultMessage: 'The link failed to open. Please try again.',
});
}
setError(
message,
);
};
tryOpenURL(url, onError);
};
useEffect(() => {
const onURLChange = ({url}: { url: string }) => {
if (url && url.startsWith(redirectUrl)) {
const parsedUrl = urlParse(url, true);
const bearerToken = parsedUrl.query?.MMAUTHTOKEN;
const csrfToken = parsedUrl.query?.MMCSRF;
if (bearerToken && csrfToken) {
setLoginSuccess(true);
doSSOLogin(bearerToken, csrfToken);
} else {
setError(
intl.formatMessage({
id: 'mobile.oauth.failed_to_login',
defaultMessage: 'Your login attempt failed. Please try again.',
}),
);
}
}
};
const listener = Linking.addEventListener('url', onURLChange);
const timeout = setTimeout(() => {
init(false);
}, 1000);
return () => {
listener.remove();
clearTimeout(timeout);
};
}, []);
let content;
if (loginSuccess) {
content = (<AuthSuccess theme={theme}/>);
} else if (loginError || error) {
content = (
<AuthError
error={loginError || error}
retry={init}
theme={theme}
/>
);
} else {
content = (<AuthRedirect theme={theme}/>);
}
return (
<View
style={style.container}
testID='sso.redirect_url'
>
{content}
</View>
);
};
export default SSOAuthenticationWithExternalBrowser;

View file

@ -702,6 +702,8 @@
"mobile.no_results_with_term.messages": "No matches found for “{term}”",
"mobile.no_results.spelling": "Check the spelling or try another search.",
"mobile.oauth.failed_to_login": "Your login attempt failed. Please try again.",
"mobile.oauth.failed_to_open_link": "The link failed to open. Please try again.",
"mobile.oauth.failed_to_open_link_no_browser": "The link failed to open. Please verify that a browser is installed on the device.",
"mobile.oauth.something_wrong.okButton": "OK",
"mobile.oauth.success.description": "Signing in now, just a moment...",
"mobile.oauth.success.title": "Authentication successful",

View file

@ -147,6 +147,7 @@ interface ClientConfig {
MaxNotificationsPerChannel: string;
MaxPostSize: string;
MinimumHashtagLength: string;
MobileExternalBrowser: string;
OpenIdButtonColor: string;
OpenIdButtonText: string;
PasswordEnableForgotLink: string;