diff --git a/src/CONFIG.ts b/src/CONFIG.ts index c02ed8065836..8b1dab5b3d71 100644 --- a/src/CONFIG.ts +++ b/src/CONFIG.ts @@ -64,6 +64,7 @@ export default { CONCIERGE_URL_PATHNAME: 'concierge/', DEVPORTAL_URL_PATHNAME: '_devportal/', CONCIERGE_URL: `${expensifyURL}concierge/`, + SAML_URL: `${expensifyURL}authentication/saml/login`, }, IS_IN_PRODUCTION: Platform.OS === 'web' ? process.env.NODE_ENV === 'production' : !__DEV__, IS_IN_STAGING: ENVIRONMENT === CONST.ENVIRONMENT.STAGING, diff --git a/src/CONST.ts b/src/CONST.ts index a4f28a9c98c7..0591aa2415c3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -243,6 +243,7 @@ const CONST = { CUSTOM_STATUS: 'customStatus', NEW_DOT_CATEGORIES: 'newDotCategories', NEW_DOT_TAGS: 'newDotTags', + NEW_DOT_SAML: 'newDotSAML', }, BUTTON_STATES: { DEFAULT: 'default', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7127c1483c26..60c526d6d885 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -36,6 +36,8 @@ export default { APPLE_SIGN_IN: 'sign-in-with-apple', GOOGLE_SIGN_IN: 'sign-in-with-google', DESKTOP_SIGN_IN_REDIRECT: 'desktop-signin-redirect', + SAML_SIGN_IN: 'sign-in-with-saml', + // This is a special validation URL that will take the user to /workspace/new after validation. This is used // when linking users from e.com in order to share a session in this app. ENABLE_PAYMENTS: 'enable-payments', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 69f905e4a7a3..99d5eb6277cd 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -29,6 +29,7 @@ export default { SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', + SAML_SIGN_IN: 'SAMLSignIn', VALIDATE_LOGIN: 'ValidateLogin', // Iframe screens from olddot diff --git a/src/languages/en.ts b/src/languages/en.ts index 9d376c73ea62..e8612c605ea0 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -381,6 +381,14 @@ export default { termsOfService: 'Terms of Service', privacy: 'Privacy', }, + samlSignIn: { + welcomeSAMLEnabled: 'Continue logging in with single sign-on:', + orContinueWithMagicCode: 'Or optionally, your company allows signing in with a magic code', + useSingleSignOn: 'Use single sign-on', + useMagicCode: 'Use magic code', + launching: 'Launching...', + oneMoment: "One moment while we redirect you to your company's single sign-on portal.", + }, reportActionCompose: { addAction: 'Actions', dropToUpload: 'Drop to upload', diff --git a/src/languages/es.ts b/src/languages/es.ts index 316cd1eaed21..e9c7d2d34f79 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -372,6 +372,14 @@ export default { termsOfService: 'Términos de servicio', privacy: 'Privacidad', }, + samlSignIn: { + welcomeSAMLEnabled: 'Continua iniciando sesión con el inicio de sesión único:', + orContinueWithMagicCode: 'O, opcionalmente, tu empresa te permite iniciar sesión con un código mágico', + useSingleSignOn: 'Usar el inicio de sesión único', + useMagicCode: 'Usar código mágico', + launching: 'Cargando...', + oneMoment: 'Un momento mientras te redirigimos al portal de inicio de sesión único de tu empresa.', + }, reportActionCompose: { addAction: 'Acción', dropToUpload: 'Suelta el archivo aquí para compartirlo', diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.js b/src/libs/Navigation/AppNavigator/PublicScreens.js index 7a87530a2d9e..7b0afb787278 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.js +++ b/src/libs/Navigation/AppNavigator/PublicScreens.js @@ -8,6 +8,7 @@ import defaultScreenOptions from './defaultScreenOptions'; import UnlinkLoginPage from '../../../pages/UnlinkLoginPage'; import AppleSignInDesktopPage from '../../../pages/signin/AppleSignInDesktopPage'; import GoogleSignInDesktopPage from '../../../pages/signin/GoogleSignInDesktopPage'; +import SAMLSignInPage from '../../../pages/signin/SAMLSignInPage'; const RootStack = createStackNavigator(); @@ -44,6 +45,11 @@ function PublicScreens() { options={defaultScreenOptions} component={GoogleSignInDesktopPage} /> + ); } diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index fde5fe400c76..9c9c4e227f41 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -15,6 +15,7 @@ export default { [SCREENS.CONCIERGE]: ROUTES.CONCIERGE, AppleSignInDesktop: ROUTES.APPLE_SIGN_IN, GoogleSignInDesktop: ROUTES.GOOGLE_SIGN_IN, + SAMLSignIn: ROUTES.SAML_SIGN_IN, [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT, [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route, diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 117a092c3875..3b623a42689d 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -316,7 +316,7 @@ function signInWithShortLivedAuthToken(email, authToken) { // If the user is signing in with a different account from the current app, should not pass the auto-generated login as it may be tied to the old account. // scene 1: the user is transitioning to newDot from a different account on oldDot. // scene 2: the user is transitioning to desktop app from a different account on web app. - const oldPartnerUserID = credentials.login === email ? credentials.autoGeneratedLogin : ''; + const oldPartnerUserID = credentials.login === email && credentials.autoGeneratedLogin ? credentials.autoGeneratedLogin : ''; API.read('SignInWithShortLivedAuthToken', {authToken, oldPartnerUserID, skipReauthentication: true}, {optimisticData, successData, failureData}); } @@ -541,6 +541,10 @@ function clearAccountMessages() { }); } +function setAccountError(error) { + Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error)}); +} + // It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to // reconnect each time when we only need to reconnect once. This way, if an authToken is expired and we try to // subscribe to a bunch of channels at once we will only reauthenticate and force reconnect Pusher once. @@ -807,6 +811,7 @@ export { unlinkLogin, clearSignInData, clearAccountMessages, + setAccountError, authenticatePusher, reauthenticatePusher, invalidateCredentials, diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.js b/src/pages/LogInWithShortLivedAuthTokenPage.js index 62eff262611d..875cdf7e8072 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.js +++ b/src/pages/LogInWithShortLivedAuthTokenPage.js @@ -12,8 +12,7 @@ import themeColors from '../styles/themes/default'; import Icon from '../components/Icon'; import * as Expensicons from '../components/Icon/Expensicons'; import * as Illustrations from '../components/Icon/Illustrations'; -import withLocalize, {withLocalizePropTypes} from '../components/withLocalize'; -import compose from '../libs/compose'; +import useLocalize from '../hooks/useLocalize'; import TextLink from '../components/TextLink'; import ONYXKEYS from '../ONYXKEYS'; @@ -33,8 +32,6 @@ const propTypes = { }), }).isRequired, - ...withLocalizePropTypes, - /** The details about the account that the user is signing in with */ account: PropTypes.shape({ /** Whether a sign is loading */ @@ -49,15 +46,26 @@ const defaultProps = { }; function LogInWithShortLivedAuthTokenPage(props) { + const {translate} = useLocalize(); + useEffect(() => { const email = lodashGet(props, 'route.params.email', ''); // We have to check for both shortLivedAuthToken and shortLivedToken, as the old mobile app uses shortLivedToken, and is not being actively updated. const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '') || lodashGet(props, 'route.params.shortLivedToken', ''); - if (shortLivedAuthToken) { + + // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts + if (shortLivedAuthToken && !props.account.isLoading) { Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken); return; } + + // If an error is returned as part of the route, ensure we set it in the onyxData for the account + const error = lodashGet(props, 'route.params.error', ''); + if (error) { + Session.setAccountError(error); + } + const exitTo = lodashGet(props, 'route.params.exitTo', ''); if (exitTo) { Navigation.isNavigationReady().then(() => { @@ -82,10 +90,18 @@ function LogInWithShortLivedAuthTokenPage(props) { src={Illustrations.RocketBlue} /> - {props.translate('deeplinkWrapper.launching')} + {translate('deeplinkWrapper.launching')} - {props.translate('deeplinkWrapper.expired')} Navigation.navigate()}>{props.translate('deeplinkWrapper.signIn')} + {translate('deeplinkWrapper.expired')}{' '} + { + Session.clearSignInData(); + Navigation.navigate(); + }} + > + {translate('deeplinkWrapper.signIn')} + @@ -105,9 +121,7 @@ LogInWithShortLivedAuthTokenPage.propTypes = propTypes; LogInWithShortLivedAuthTokenPage.defaultProps = defaultProps; LogInWithShortLivedAuthTokenPage.displayName = 'LogInWithShortLivedAuthTokenPage'; -export default compose( - withLocalize, - withOnyx({ - account: {key: ONYXKEYS.ACCOUNT}, - }), -)(LogInWithShortLivedAuthTokenPage); +export default withOnyx({ + account: {key: ONYXKEYS.ACCOUNT}, + session: {key: ONYXKEYS.SESSION}, +})(LogInWithShortLivedAuthTokenPage); diff --git a/src/pages/signin/ChooseSSOOrMagicCode.js b/src/pages/signin/ChooseSSOOrMagicCode.js new file mode 100644 index 000000000000..32f0776cdbc9 --- /dev/null +++ b/src/pages/signin/ChooseSSOOrMagicCode.js @@ -0,0 +1,108 @@ +import React from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import _ from 'underscore'; +import styles from '../../styles/styles'; +import ONYXKEYS from '../../ONYXKEYS'; +import Text from '../../components/Text'; +import Button from '../../components/Button'; +import * as Session from '../../libs/actions/Session'; +import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; +import Terms from './Terms'; +import CONST from '../../CONST'; +import ROUTES from '../../ROUTES'; +import Navigation from '../../libs/Navigation/Navigation'; +import * as ErrorUtils from '../../libs/ErrorUtils'; +import useLocalize from '../../hooks/useLocalize'; +import useNetwork from '../../hooks/useNetwork'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; +import FormHelpMessage from '../../components/FormHelpMessage'; + +const propTypes = { + /* Onyx Props */ + + /** The credentials of the logged in person */ + credentials: PropTypes.shape({ + /** The email/phone the user logged in with */ + login: PropTypes.string, + }), + + /** The details about the account that the user is signing in with */ + account: PropTypes.shape({ + /** Whether or not a sign on form is loading (being submitted) */ + isLoading: PropTypes.bool, + + /** Form that is being loaded */ + loadingForm: PropTypes.oneOf(_.values(CONST.FORMS)), + + /** Whether this account has 2FA enabled or not */ + requiresTwoFactorAuth: PropTypes.bool, + + /** Server-side errors in the submitted authentication code */ + errors: PropTypes.objectOf(PropTypes.string), + }), + + /** Function that returns whether the user is using SAML or magic codes to log in */ + setIsUsingMagicCode: PropTypes.func.isRequired, +}; + +const defaultProps = { + credentials: {}, + account: {}, +}; + +function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const {isSmallScreenWidth} = useWindowDimensions(); + + return ( + <> + + {translate('samlSignIn.welcomeSAMLEnabled')} +