Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate SignInPage to function component ... again #20708

Merged
merged 4 commits into from
Jul 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 [withReportCommentDrafts, ReportCommentDraftsProvider] = createOnyxContext(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT);

const propTypes = {
Expand Down Expand Up @@ -42,4 +42,4 @@ OnyxProvider.propTypes = propTypes;

export default OnyxProvider;

export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext, withReportCommentDrafts};
export {withNetwork, withPersonalDetails, withReportActionsDrafts, withCurrentDate, withBlockedFromConcierge, withBetas, NetworkContext, BetasContext, withReportCommentDrafts};
15 changes: 15 additions & 0 deletions src/hooks/usePermissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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(() => {
const permissions = {};
_.each(Permissions, (checkerFunction, beta) => {
permissions[beta] = checkerFunction(betas);
});
return permissions;
}, [betas]);
}
257 changes: 125 additions & 132 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 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 * as StyleUtils from '../../styles/StyleUtils';
import useLocalize from '../../hooks/useLocalize';
import usePermissions from '../../hooks/usePermissions';
import useWindowDimensions from '../../hooks/useWindowDimensions';
import Log from '../../libs/Log';

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({credentials, account}) {
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);