open an external browser when the server config for MobileExternalBrowse is set to true (#8220)
This commit is contained in:
parent
af31eed17c
commit
f04838d9b1
8 changed files with 395 additions and 99 deletions
75
app/screens/sso/components/auth_error.tsx
Normal file
75
app/screens/sso/components/auth_error.tsx
Normal 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;
|
||||
55
app/screens/sso/components/auth_redirect.tsx
Normal file
55
app/screens/sso/components/auth_redirect.tsx
Normal 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;
|
||||
57
app/screens/sso/components/auth_success.tsx
Normal file
57
app/screens/sso/components/auth_success.tsx
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
166
app/screens/sso/sso_authentication_with_external_browser.tsx
Normal file
166
app/screens/sso/sso_authentication_with_external_browser.tsx
Normal 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;
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
1
types/api/config.d.ts
vendored
1
types/api/config.d.ts
vendored
|
|
@ -147,6 +147,7 @@ interface ClientConfig {
|
|||
MaxNotificationsPerChannel: string;
|
||||
MaxPostSize: string;
|
||||
MinimumHashtagLength: string;
|
||||
MobileExternalBrowser: string;
|
||||
OpenIdButtonColor: string;
|
||||
OpenIdButtonText: string;
|
||||
PasswordEnableForgotLink: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue