diff --git a/app/screens/sso/components/auth_error.tsx b/app/screens/sso/components/auth_error.tsx
new file mode 100644
index 00000000000..897ac7d3e64
--- /dev/null
+++ b/app/screens/sso/components/auth_error.tsx
@@ -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 (
+
+
+
+ {`${error}.`}
+
+
+
+ );
+};
+
+export default AuthError;
diff --git a/app/screens/sso/components/auth_redirect.tsx b/app/screens/sso/components/auth_redirect.tsx
new file mode 100644
index 00000000000..f374aa9d4cb
--- /dev/null
+++ b/app/screens/sso/components/auth_redirect.tsx
@@ -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 (
+
+
+
+
+ );
+};
+
+export default AuthRedirect;
diff --git a/app/screens/sso/components/auth_success.tsx b/app/screens/sso/components/auth_success.tsx
new file mode 100644
index 00000000000..2550450b939
--- /dev/null
+++ b/app/screens/sso/components/auth_success.tsx
@@ -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 (
+
+
+
+
+
+ );
+};
+
+export default AuthSuccess;
diff --git a/app/screens/sso/index.tsx b/app/screens/sso/index.tsx
index 1d127d5dd38..6cabcbf47c8 100644
--- a/app/screens/sso/index.tsx
+++ b/app/screens/sso/index.tsx
@@ -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 = (
+
+ );
+ } else {
+ authentication = (
+
+ );
+ }
+
return (
-
+ {authentication}
);
diff --git a/app/screens/sso/sso_authentication.tsx b/app/screens/sso/sso_authentication.tsx
index 5e6d6fbc4b0..c60007ce01c 100644
--- a/app/screens/sso/sso_authentication.tsx
+++ b/app/screens/sso/sso_authentication.tsx
@@ -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('');
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 = (
-
-
-
-
-
- );
+ content = ();
} else if (loginError || error) {
content = (
-
-
-
- {`${loginError || error}.`}
-
-
-
+
);
} else {
- content = (
-
-
-
-
- );
+ content = ();
}
return (
diff --git a/app/screens/sso/sso_authentication_with_external_browser.tsx b/app/screens/sso/sso_authentication_with_external_browser.tsx
new file mode 100644
index 00000000000..af439c23b5b
--- /dev/null
+++ b/app/screens/sso/sso_authentication_with_external_browser.tsx
@@ -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('');
+ 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 = {
+ ...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 = ();
+ } else if (loginError || error) {
+ content = (
+
+ );
+ } else {
+ content = ();
+ }
+
+ return (
+
+ {content}
+
+ );
+};
+
+export default SSOAuthenticationWithExternalBrowser;
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index 6adffcd30af..f3ce56964e7 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -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",
diff --git a/types/api/config.d.ts b/types/api/config.d.ts
index 3354f41e51a..29b95a32738 100644
--- a/types/api/config.d.ts
+++ b/types/api/config.d.ts
@@ -147,6 +147,7 @@ interface ClientConfig {
MaxNotificationsPerChannel: string;
MaxPostSize: string;
MinimumHashtagLength: string;
+ MobileExternalBrowser: string;
OpenIdButtonColor: string;
OpenIdButtonText: string;
PasswordEnableForgotLink: string;