Skip to content

Commit

Permalink
Merge pull request #29526 from Expensify/nikki-saml-newdot-ios
Browse files Browse the repository at this point in the history
Add iOS and Android view for SAML Login
  • Loading branch information
arosiclair authored Dec 4, 2023
2 parents e105439 + 01c4029 commit 5dcdd09
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 55 deletions.
42 changes: 42 additions & 0 deletions src/components/SAMLLoadingIndicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';
import {StyleSheet, View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import styles from '@styles/styles';
import themeColors from '@styles/themes/default';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import * as Illustrations from './Icon/Illustrations';
import Text from './Text';

function SAMLLoadingIndicator() {
const {translate} = useLocalize();
return (
<View style={[StyleSheet.absoluteFillObject, styles.deeplinkWrapperContainer]}>
<View style={styles.deeplinkWrapperMessage}>
<View style={styles.mb2}>
<Icon
width={200}
height={164}
src={Illustrations.RocketBlue}
/>
</View>
<Text style={[styles.textHeadline, styles.textXXLarge, styles.textAlignCenter]}>{translate('samlSignIn.launching')}</Text>
<View style={[styles.mt2, styles.mh2, styles.fontSizeNormal, styles.textAlignCenter]}>
<Text style={[styles.textAlignCenter]}>{translate('samlSignIn.oneMoment')}</Text>
</View>
</View>
<View style={styles.deeplinkWrapperFooter}>
<Icon
width={154}
height={34}
fill={themeColors.success}
src={Expensicons.ExpensifyWordmark}
/>
</View>
</View>
);
}

SAMLLoadingIndicator.displayName = 'SAMLLoadingIndicator';

export default SAMLLoadingIndicator;
1 change: 0 additions & 1 deletion src/pages/LogInWithShortLivedAuthTokenPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,4 @@ LogInWithShortLivedAuthTokenPage.displayName = 'LogInWithShortLivedAuthTokenPage

export default withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
session: {key: ONYXKEYS.SESSION},
})(LogInWithShortLivedAuthTokenPage);
39 changes: 2 additions & 37 deletions src/pages/signin/SAMLSignInPage/index.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import PropTypes from 'prop-types';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import * as Illustrations from '@components/Icon/Illustrations';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';
import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';

Expand All @@ -25,39 +18,11 @@ const defaultProps = {
};

function SAMLSignInPage({credentials}) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();

useEffect(() => {
window.open(`${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}`, '_self');
}, [credentials.login]);

return (
<View style={styles.deeplinkWrapperContainer}>
<View style={styles.deeplinkWrapperMessage}>
<View style={styles.mb2}>
<Icon
width={200}
height={164}
src={Illustrations.RocketBlue}
/>
</View>
<Text style={[styles.textHeadline, styles.textXXLarge, styles.textAlignCenter]}>{translate('samlSignIn.launching')}</Text>
<View style={[styles.mt2, styles.mh2, styles.fontSizeNormal, styles.textAlignCenter]}>
<Text style={[styles.textAlignCenter]}>{translate('samlSignIn.oneMoment')}</Text>
</View>
</View>
<View style={styles.deeplinkWrapperFooter}>
<Icon
width={154}
height={34}
fill={theme.success}
src={Expensicons.ExpensifyWordmark}
/>
</View>
</View>
);
return <SAMLLoadingIndicator />;
}

SAMLSignInPage.propTypes = propTypes;
Expand Down
95 changes: 95 additions & 0 deletions src/pages/signin/SAMLSignInPage/index.native.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import PropTypes from 'prop-types';
import React, {useCallback, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import WebView from 'react-native-webview';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator';
import ScreenWrapper from '@components/ScreenWrapper';
import getPlatform from '@libs/getPlatform';
import Navigation from '@libs/Navigation/Navigation';
import * as Session from '@userActions/Session';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

const propTypes = {
/** The credentials of the logged in person */
credentials: PropTypes.shape({
/** The email/phone the user logged in with */
login: PropTypes.string,
}),
};

const defaultProps = {
credentials: {},
};

function SAMLSignInPage({credentials}) {
const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`;
const [showNavigation, shouldShowNavigation] = useState(true);

/**
* Handles in-app navigation once we get a response back from Expensify
*
* @param {String} params.url
*/
const handleNavigationStateChange = useCallback(
({url}) => {
// If we've gotten a callback then remove the option to navigate back to the sign in page
if (url.includes('loginCallback')) {
shouldShowNavigation(false);
}

const searchParams = new URLSearchParams(new URL(url).search);
if (searchParams.has('shortLivedAuthToken')) {
const shortLivedAuthToken = searchParams.get('shortLivedAuthToken');
Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken);
}

// If the login attempt is unsuccessful, set the error message for the account and redirect to sign in page
if (searchParams.has('error')) {
Session.clearSignInData();
Session.setAccountError(searchParams.get('error'));
Navigation.navigate(ROUTES.HOME);
}
},
[credentials.login, shouldShowNavigation],
);

return (
<ScreenWrapper
shouldShowOfflineIndicator={false}
includeSafeAreaPaddingBottom={false}
testID={SAMLSignInPage.displayName}
>
{showNavigation && (
<HeaderWithBackButton
title=""
onBackButtonPress={() => {
Session.clearSignInData();
Navigation.navigate(ROUTES.HOME);
}}
/>
)}
<FullPageOfflineBlockingView>
<WebView
originWhitelist={['https://*']}
source={{uri: samlLoginURL}}
incognito // 'incognito' prop required for Android, issue here https://github.com/react-native-webview/react-native-webview/issues/1352
startInLoadingState
renderLoading={() => <SAMLLoadingIndicator />}
onNavigationStateChange={handleNavigationStateChange}
/>
</FullPageOfflineBlockingView>
</ScreenWrapper>
);
}

SAMLSignInPage.propTypes = propTypes;
SAMLSignInPage.defaultProps = defaultProps;
SAMLSignInPage.displayName = 'SAMLSignInPage';

export default withOnyx({
credentials: {key: ONYXKEYS.CREDENTIALS},
})(SAMLSignInPage);
29 changes: 17 additions & 12 deletions src/pages/signin/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import useLocalize from '@hooks/useLocalize';
import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as ActiveClientManager from '@libs/ActiveClientManager';
import getPlatform from '@libs/getPlatform';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -103,15 +102,9 @@ function getRenderOptions({hasLogin, hasValidateCode, account, isPrimaryLogin, i
const isSAMLRequired = Boolean(account.isSAMLRequired);
const hasEmailDeliveryFailure = Boolean(account.hasEmailDeliveryFailure);

// SAML is temporarily restricted to users on the beta or to users signing in on web and mweb
let shouldShowChooseSSOOrMagicCode = false;
let shouldInitiateSAMLLogin = false;
const platform = getPlatform();
if (platform === CONST.PLATFORM.WEB || platform === CONST.PLATFORM.DESKTOP) {
// True if the user has SAML required and we haven't already initiated SAML for their account
shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading;
shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode;
}
// True if the user has SAML required and we haven't already initiated SAML for their account
const shouldInitiateSAMLLogin = hasAccount && hasLogin && isSAMLRequired && !hasInitiatedSAMLLogin && account.isLoading;
const shouldShowChooseSSOOrMagicCode = hasAccount && hasLogin && isSAMLEnabled && !isSAMLRequired && !isUsingMagicCode;

// SAML required users may reload the login page after having already entered their login details, in which
// case we want to clear their sign in data so they don't end up in an infinite loop redirecting back to their
Expand Down Expand Up @@ -167,6 +160,19 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
}
App.setLocale(Localize.getDevicePreferredLocale());
}, [preferredLocale]);
useEffect(() => {
if (credentials.login) {
return;
}

// If we don't have a login set, reset the user's SAML login preferences
if (isUsingMagicCode) {
setIsUsingMagicCode(false);
}
if (hasInitiatedSAMLLogin) {
setHasInitiatedSAMLLogin(false);
}
}, [credentials.login, isUsingMagicCode, setIsUsingMagicCode, hasInitiatedSAMLLogin, setHasInitiatedSAMLLogin]);

const {
shouldShowLoginForm,
Expand Down Expand Up @@ -231,7 +237,7 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
if (shouldShowEmailDeliveryFailurePage || shouldShowChooseSSOOrMagicCode) {
welcomeText = '';
}
} else if (!shouldInitiateSAMLLogin) {
} else if (!shouldInitiateSAMLLogin && !hasInitiatedSAMLLogin) {
Log.warn('SignInPage in unexpected state!');
}

Expand Down Expand Up @@ -261,7 +267,6 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
<ValidateCodeForm
isUsingRecoveryCode={isUsingRecoveryCode}
setIsUsingRecoveryCode={setIsUsingRecoveryCode}
setIsUsingMagicCode={setIsUsingMagicCode}
/>
)}
{shouldShowUnlinkLoginForm && <UnlinkLoginForm />}
Expand Down
5 changes: 0 additions & 5 deletions src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ const propTypes = {
/** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */
setIsUsingRecoveryCode: PropTypes.func.isRequired,

/** Function to change `isUsingMagicCode` state when the user goes back to the login page */
setIsUsingMagicCode: PropTypes.func.isRequired,

...withLocalizePropTypes,
};

Expand Down Expand Up @@ -210,8 +207,6 @@ function BaseValidateCodeForm(props) {
* Clears local and Onyx sign in states
*/
const clearSignInData = () => {
// Reset the user's preference for signing in with SAML versus magic codes
props.setIsUsingMagicCode(false);
clearLocalSignInData();
Session.clearSignInData();
};
Expand Down

0 comments on commit 5dcdd09

Please sign in to comment.