Skip to content

Commit

Permalink
Merge pull request #19498 from Expensify/Rory-HookySignInPage
Browse files Browse the repository at this point in the history
Migrate SignInPage to functional component
  • Loading branch information
arosiclair authored Jun 8, 2023
2 parents 81b631b + 3c58737 commit 38ba7c8
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 138 deletions.
4 changes: 2 additions & 2 deletions src/components/OnyxProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const [withPersonalDetails, PersonalDetailsProvider] = createOnyxContext(ONYXKEY
const [withCurrentDate, CurrentDateProvider] = createOnyxContext(ONYXKEYS.CURRENT_DATE);
const [withReportActionsDrafts, ReportActionsDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS);
const [withBlockedFromConcierge, BlockedFromConciergeProvider] = createOnyxContext(ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE);
const [withBetas, BetasProvider] = createOnyxContext(ONYXKEYS.BETAS);
const [withBetas, BetasProvider, BetasContext] = createOnyxContext(ONYXKEYS.BETAS);

const propTypes = {
/** Rendered child component */
Expand All @@ -29,4 +29,4 @@ OnyxProvider.propTypes = propTypes;

export default OnyxProvider;

export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext};
export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext, BetasContext};
2 changes: 1 addition & 1 deletion src/components/withLocalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,4 @@ export default function withLocalize(WrappedComponent) {
return WithLocalize;
}

export {withLocalizePropTypes, Provider as LocaleContextProvider};
export {withLocalizePropTypes, Provider as LocaleContextProvider, LocaleContext};
6 changes: 6 additions & 0 deletions src/hooks/useLocalize.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {useContext} from 'react';
import {LocaleContext} from '../components/withLocalize';

export default function useLocalize() {
return useContext(LocaleContext);
}
21 changes: 21 additions & 0 deletions src/hooks/usePermissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import _ from 'underscore';
import {useContext, useMemo} from 'react';
import Permissions from '../libs/Permissions';
import {BetasContext} from '../components/OnyxProvider';

export default function usePermissions() {
const betas = useContext(BetasContext);
return useMemo(
() =>
_.reduce(
Permissions,
(memo, checkerFunction, beta) => {
// eslint-disable-next-line no-param-reassign
memo[beta] = checkerFunction(betas);
return memo;
},
{},
),
[betas],
);
}
259 changes: 126 additions & 133 deletions src/pages/signin/SignInPage.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import React, {Component} from 'react';
import React, {useEffect} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import {View} from 'react-native';
import lodashGet from 'lodash/get';
import Str from 'expensify-common/lib/str';
import {withSafeAreaInsets} from 'react-native-safe-area-context';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import ONYXKEYS from '../../ONYXKEYS';
import styles from '../../styles/styles';
import compose from '../../libs/compose';
import SignInPageLayout from './SignInPageLayout';
import LoginForm from './LoginForm';
import PasswordForm from './PasswordForm';
import ValidateCodeForm from './ValidateCodeForm';
import ResendValidationForm from './ResendValidationForm';
import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize';
import Performance from '../../libs/Performance';
import * as App from '../../libs/actions/App';
import Permissions from '../../libs/Permissions';
import UnlinkLoginForm from './UnlinkLoginForm';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions';
import * as Localize from '../../libs/Localize';
import useLocalize from '../../hooks/useLocalize';
import usePermissions from '../../hooks/usePermissions';
import useWindowDimensions from '../../hooks/useWindowDimensions';
import Log from '../../libs/Log';
import * as StyleUtils from '../../styles/StyleUtils';

const propTypes = {
/* Onyx Props */

/** The details about the account that the user is signing in with */
account: PropTypes.shape({
/** Error to display when there is an account error returned */
Expand All @@ -35,153 +32,149 @@ const propTypes = {

/** The primaryLogin associated with the account */
primaryLogin: PropTypes.string,
}),

/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),
/** Has the user pressed the forgot password button? */
forgotPassword: PropTypes.bool,

/** Does this account require 2FA? */
requiresTwoFactorAuth: PropTypes.bool,
}),

/** The credentials of the person signing in */
credentials: PropTypes.shape({
login: PropTypes.string,
password: PropTypes.string,
twoFactorAuthCode: PropTypes.string,
validateCode: PropTypes.string,
}),

...withLocalizePropTypes,

...windowDimensionsPropTypes,
};

const defaultProps = {
account: {},
betas: [],
credentials: {},
};

class SignInPage extends Component {
componentDidMount() {
Performance.measureTTI();
/**
* @param {Boolean} hasLogin
* @param {Boolean} hasPassword
* @param {Boolean} hasValidateCode
* @param {Boolean} isPrimaryLogin
* @param {Boolean} isAccountValidated
* @param {Boolean} didForgetPassword
* @param {Boolean} canUsePasswordlessLogins
* @returns {Object}
*/
function getRenderOptions({hasLogin, hasPassword, hasValidateCode, isPrimaryLogin, isAccountValidated, didForgetPassword, canUsePasswordlessLogins}) {
const shouldShowLoginForm = !hasLogin && !hasValidateCode;
const isUnvalidatedSecondaryLogin = hasLogin && !isPrimaryLogin && !isAccountValidated;
const shouldShowPasswordForm = hasLogin && isAccountValidated && !hasPassword && !didForgetPassword && !isUnvalidatedSecondaryLogin && !canUsePasswordlessLogins;
const shouldShowValidateCodeForm = (hasLogin || hasValidateCode) && !isUnvalidatedSecondaryLogin && canUsePasswordlessLogins;
const shouldShowResendValidationForm = hasLogin && (!isAccountValidated || didForgetPassword) && !isUnvalidatedSecondaryLogin && !canUsePasswordlessLogins;
const shouldShowWelcomeHeader = shouldShowLoginForm || shouldShowPasswordForm || shouldShowValidateCodeForm || isUnvalidatedSecondaryLogin;
const shouldShowWelcomeText = shouldShowLoginForm || shouldShowPasswordForm || shouldShowValidateCodeForm;
return {
shouldShowLoginForm,
shouldShowUnlinkLoginForm: isUnvalidatedSecondaryLogin,
shouldShowPasswordForm,
shouldShowValidateCodeForm,
shouldShowResendValidationForm,
shouldShowWelcomeHeader,
shouldShowWelcomeText,
};
}

App.setLocale(Localize.getDevicePreferredLocale());
}
function SignInPage({account, credentials}) {
const {translate, formatPhoneNumber} = useLocalize();
const {canUsePasswordlessLogins} = usePermissions();
const {isSmallScreenWidth} = useWindowDimensions();
const safeAreaInsets = useSafeAreaInsets();

render() {
// Show the login form if
// - A login has not been entered yet
// - AND a validateCode has not been cached with sign in link
const showLoginForm = !this.props.credentials.login && !this.props.credentials.validateCode;

// Show the unlink form if
// - A login has been entered
// - AND the login is not the primary login
// - AND the login is not validated
const showUnlinkLoginForm =
Boolean(this.props.credentials.login && this.props.account.primaryLogin) && this.props.account.primaryLogin !== this.props.credentials.login && !this.props.account.validated;

// Show the old password form if
// - A login has been entered
// - AND an account exists and is validated for this login
// - AND a password hasn't been entered yet
// - AND haven't forgotten password
// - AND the login isn't an unvalidated secondary login
// - AND the user is NOT on the passwordless beta
const showPasswordForm =
Boolean(this.props.credentials.login) &&
this.props.account.validated &&
!this.props.credentials.password &&
!this.props.account.forgotPassword &&
!showUnlinkLoginForm &&
!Permissions.canUsePasswordlessLogins(this.props.betas);

// Show the new magic code / validate code form if
// - A login has been entered or a validateCode has been cached from sign in link
// - AND the login isn't an unvalidated secondary login
// - AND the user is on the 'passwordless' beta
const showValidateCodeForm =
Boolean(this.props.credentials.login || this.props.credentials.validateCode) && !showUnlinkLoginForm && Permissions.canUsePasswordlessLogins(this.props.betas);

// Show the resend validation link form if
// - A login has been entered
// - AND is not validated or password is forgotten
// - AND the login isn't an unvalidated secondary login
// - AND user is not on 'passwordless' beta
const showResendValidationForm =
Boolean(this.props.credentials.login) &&
(!this.props.account.validated || this.props.account.forgotPassword) &&
!showUnlinkLoginForm &&
!Permissions.canUsePasswordlessLogins(this.props.betas);

let welcomeHeader = '';
let welcomeText = '';
if (showValidateCodeForm) {
if (this.props.account.requiresTwoFactorAuth) {
// We will only know this after a user signs in successfully, without their 2FA code
welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack');
welcomeText = this.props.translate('validateCodeForm.enterAuthenticatorCode');
useEffect(() => Performance.measureTTI(), []);
useEffect(() => {
App.setLocale(Localize.getDevicePreferredLocale());
}, []);

const {
shouldShowLoginForm,
shouldShowUnlinkLoginForm,
shouldShowPasswordForm,
shouldShowValidateCodeForm,
shouldShowResendValidationForm,
shouldShowWelcomeHeader,
shouldShowWelcomeText,
} = getRenderOptions({
hasLogin: Boolean(credentials.login),
hasPassword: Boolean(credentials.password),
hasValidateCode: Boolean(credentials.validateCode),
isPrimaryLogin: account.primaryLogin && account.primaryLogin === credentials.login,
isAccountValidated: Boolean(account.validated),
didForgetPassword: Boolean(account.forgotPassword),
canUsePasswordlessLogins,
});

let welcomeHeader;
let welcomeText;
if (shouldShowValidateCodeForm) {
if (account.requiresTwoFactorAuth) {
// We will only know this after a user signs in successfully, without their 2FA code
welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
welcomeText = translate('validateCodeForm.enterAuthenticatorCode');
} else {
const userLogin = Str.removeSMSDomain(credentials.login || '');

// replacing spaces with "hard spaces" to prevent breaking the number
const userLoginToDisplay = Str.isSMSLogin(userLogin) ? formatPhoneNumber(userLogin).replace(/ /g, '\u00A0') : userLogin;
if (account.validated) {
welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
welcomeText = isSmallScreenWidth
? `${translate('welcomeText.welcomeBack')} ${translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay})}`
: translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay});
} else {
const userLogin = Str.removeSMSDomain(lodashGet(this.props, 'credentials.login', ''));

// replacing spaces with "hard spaces" to prevent breaking the number
const userLoginToDisplay = Str.isSMSLogin(userLogin) ? this.props.formatPhoneNumber(userLogin).replace(/ /g, '\u00A0') : userLogin;
if (this.props.account.validated) {
welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack');
welcomeText = this.props.isSmallScreenWidth
? `${this.props.translate('welcomeText.welcomeBack')} ${this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay})}`
: this.props.translate('welcomeText.welcomeEnterMagicCode', {login: userLoginToDisplay});
} else {
welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcome');
welcomeText = this.props.isSmallScreenWidth
? `${this.props.translate('welcomeText.welcome')} ${this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay})}`
: this.props.translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay});
}
welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcome');
welcomeText = isSmallScreenWidth
? `${translate('welcomeText.welcome')} ${translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay})}`
: translate('welcomeText.newFaceEnterMagicCode', {login: userLoginToDisplay});
}
} else if (showPasswordForm) {
welcomeHeader = this.props.isSmallScreenWidth ? '' : this.props.translate('welcomeText.welcomeBack');
welcomeText = this.props.isSmallScreenWidth
? `${this.props.translate('welcomeText.welcomeBack')} ${this.props.translate('welcomeText.enterPassword')}`
: this.props.translate('welcomeText.enterPassword');
} else if (showUnlinkLoginForm) {
welcomeHeader = this.props.isSmallScreenWidth ? this.props.translate('login.hero.header') : this.props.translate('welcomeText.welcomeBack');
} else if (!showResendValidationForm) {
welcomeHeader = this.props.isSmallScreenWidth ? this.props.translate('login.hero.header') : this.props.translate('welcomeText.getStarted');
welcomeText = this.props.isSmallScreenWidth ? this.props.translate('welcomeText.getStarted') : '';
}
} else if (shouldShowPasswordForm) {
welcomeHeader = isSmallScreenWidth ? '' : translate('welcomeText.welcomeBack');
welcomeText = isSmallScreenWidth ? `${translate('welcomeText.welcomeBack')} ${translate('welcomeText.enterPassword')}` : translate('welcomeText.enterPassword');
} else if (shouldShowUnlinkLoginForm) {
welcomeHeader = isSmallScreenWidth ? translate('login.hero.header') : translate('welcomeText.welcomeBack');
} else if (!shouldShowResendValidationForm) {
welcomeHeader = isSmallScreenWidth ? translate('login.hero.header') : translate('welcomeText.getStarted');
welcomeText = isSmallScreenWidth ? translate('welcomeText.getStarted') : '';
} else {
Log.warn('SignInPage in unexpected state!');
}

return (
// There is an issue SafeAreaView on Android where wrong insets flicker on app start.
// Can be removed once https://github.com/th3rdwave/react-native-safe-area-context/issues/364 is resolved.
<View style={[styles.signInPage, StyleUtils.getSafeAreaPadding(this.props.insets, 1)]}>
<SignInPageLayout
welcomeHeader={welcomeHeader}
welcomeText={welcomeText}
shouldShowWelcomeHeader={showLoginForm || showPasswordForm || showValidateCodeForm || showUnlinkLoginForm || !this.props.isSmallScreenWidth}
shouldShowWelcomeText={showLoginForm || showPasswordForm || showValidateCodeForm}
>
{/* LoginForm and PasswordForm must use the isVisible prop. This keeps them mounted, but visually hidden
return (
<View style={[styles.signInPage, StyleUtils.getSafeAreaPadding(safeAreaInsets, 1)]}>
<SignInPageLayout
welcomeHeader={welcomeHeader}
welcomeText={welcomeText}
shouldShowWelcomeHeader={shouldShowWelcomeHeader || !isSmallScreenWidth}
shouldShowWelcomeText={shouldShowWelcomeText}
>
{/* LoginForm and PasswordForm must use the isVisible prop. This keeps them mounted, but visually hidden
so that password managers can access the values. Conditionally rendering these components will break this feature. */}
<LoginForm
isVisible={showLoginForm}
blurOnSubmit={this.props.account.validated === false}
/>
{showValidateCodeForm ? <ValidateCodeForm isVisible={showValidateCodeForm} /> : <PasswordForm isVisible={showPasswordForm} />}
{showResendValidationForm && <ResendValidationForm />}
{showUnlinkLoginForm && <UnlinkLoginForm />}
</SignInPageLayout>
</View>
);
}
<LoginForm
isVisible={shouldShowLoginForm}
blurOnSubmit={account.validated === false}
/>
{shouldShowValidateCodeForm ? <ValidateCodeForm isVisible={shouldShowValidateCodeForm} /> : <PasswordForm isVisible={shouldShowPasswordForm} />}
{shouldShowResendValidationForm && <ResendValidationForm />}
{shouldShowUnlinkLoginForm && <UnlinkLoginForm />}
</SignInPageLayout>
</View>
);
}

SignInPage.propTypes = propTypes;
SignInPage.defaultProps = defaultProps;
SignInPage.displayName = 'SignInPage';

export default compose(
withSafeAreaInsets,
withLocalize,
withWindowDimensions,
withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
betas: {key: ONYXKEYS.BETAS},
credentials: {key: ONYXKEYS.CREDENTIALS},
}),
)(SignInPage);
export default withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
credentials: {key: ONYXKEYS.CREDENTIALS},
})(SignInPage);
4 changes: 2 additions & 2 deletions src/pages/signin/UnlinkLoginForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ const defaultProps = {
};

const UnlinkLoginForm = (props) => {
const primaryLogin = Str.isSMSLogin(props.account.primaryLogin) ? Str.removeSMSDomain(props.account.primaryLogin) : props.account.primaryLogin;
const secondaryLogin = Str.isSMSLogin(props.credentials.login) ? Str.removeSMSDomain(props.credentials.login) : props.credentials.login;
const primaryLogin = Str.isSMSLogin(props.account.primaryLogin || '') ? Str.removeSMSDomain(props.account.primaryLogin || '') : props.account.primaryLogin;
const secondaryLogin = Str.isSMSLogin(props.credentials.login || '') ? Str.removeSMSDomain(props.credentials.login || '') : props.credentials.login;

return (
<>
Expand Down

0 comments on commit 38ba7c8

Please sign in to comment.