Skip to content

Commit

Permalink
Merge pull request #13655 from Expensify/beaman-addConditionalPasswor…
Browse files Browse the repository at this point in the history
…dlessFlow

Add conditional passwordless flow
  • Loading branch information
NikkiWines authored Feb 1, 2023
2 parents be6dfe2 + b67c710 commit e1c554a
Show file tree
Hide file tree
Showing 10 changed files with 340 additions and 17 deletions.
4 changes: 4 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ const CONST = {
IOU_SEND: 'sendMoney',
POLICY_ROOMS: 'policyRooms',
POLICY_EXPENSE_CHAT: 'policyExpenseChat',
PASSWORDLESS: 'passwordless',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down Expand Up @@ -463,6 +464,9 @@ const CONST = {
// at least 8 characters, 1 capital letter, 1 lowercase number, 1 number
PASSWORD_COMPLEXITY_REGEX_STRING: '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$',

// 6 numeric digits
VALIDATE_CODE_REGEX_STRING: /^\d{6}$/,

PASSWORD_PAGE: {
ERROR: {
ALREADY_VALIDATED: 'Account already validated',
Expand Down
16 changes: 15 additions & 1 deletion src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {
save: 'Save',
saveChanges: 'Save changes',
password: 'Password',
magicCode: 'Magic code',
workspaces: 'Workspaces',
profile: 'Profile',
payments: 'Payments',
Expand Down Expand Up @@ -165,10 +166,12 @@ export default {
hello: 'Hello',
phoneCountryCode: '1',
welcomeText: {
welcome: 'Welcome to the New Expensify! Enter your phone number or email to continue.',
welcome: 'Welcome to New Expensify! Enter your phone number or email to continue.',
welcomeEnterMagicCode: ({login}) => `It's always great to see a new face around here! Please enter the magic code sent to ${login}`,
phrase2: 'Money talks. And now that chat and payments are in one place, it\'s also easy.',
phrase3: 'Your payments get to you as fast as you can get your point across.',
welcomeBack: 'Welcome back to the New Expensify! Please enter your password.',
welcomeBackEnterMagicCode: ({login}) => `Welcome back! Please enter the magic code sent to ${login}`,
},
reportActionCompose: {
addAction: 'Actions',
Expand Down Expand Up @@ -523,6 +526,17 @@ export default {
phrase5: 'Money transmission is provided by Expensify Payments LLC (NMLS ID:2017010) pursuant to its',
phrase6: 'licenses',
},
validateCodeForm: {
magicCodeNotReceived: "Didn't receive a magic code?",
enterAuthenticatorCode: 'Please enter your authenticator code',
twoFactorCode: 'Two factor code',
requiredWhen2FAEnabled: 'Required when 2FA is enabled',
error: {
pleaseFillMagicCode: 'Please enter your magic code',
incorrectMagicCode: 'Incorrect magic code.',
pleaseFillTwoFactorAuth: 'Please enter your two factor code',
},
},
passwordForm: {
pleaseFillOutAllFields: 'Please fill out all fields',
pleaseFillPassword: 'Please enter your password',
Expand Down
14 changes: 14 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default {
save: 'Guardar',
saveChanges: 'Guardar cambios',
password: 'Contraseña',
magicCode: 'Código mágico',
workspaces: 'Espacios de trabajo',
profile: 'Perfil',
payments: 'Pagos',
Expand Down Expand Up @@ -166,9 +167,11 @@ export default {
phoneCountryCode: '34',
welcomeText: {
welcome: 'Con el Nuevo Expensify, chat y pagos son lo mismo.',
welcomeEnterMagicCode: ({login}) => `¡Siempre es genial ver una cara nueva por aquí! Por favor ingresa el código mágico enviado a ${login}`,
phrase2: 'El dinero habla. Y ahora que chat y pagos están en un mismo lugar, es también fácil.',
phrase3: 'Tus pagos llegan tan rápido como tus mensajes.',
welcomeBack: '¡Bienvenido de vuelta al Nuevo Expensify! Por favor, introduce tu contraseña.',
welcomeBackEnterMagicCode: ({login}) => `¡Bienvenido de nuevo! Por favor, introduce el código mágico enviado a ${login}`,
},
reportActionCompose: {
addAction: 'Acción',
Expand Down Expand Up @@ -523,6 +526,17 @@ export default {
phrase5: 'El envío de dinero es brindado por Expensify Payments LLC (NMLS ID:2017010) de conformidad con sus',
phrase6: 'licencias',
},
validateCodeForm: {
magicCodeNotReceived: '¿No recibiste un código mágico?',
enterAuthenticatorCode: 'Por favor ingresa su código de autenticador',
twoFactorCode: 'Autenticación de 2 factores',
requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado',
error: {
pleaseFillMagicCode: 'Por favor, introduce el código mágico',
incorrectMagicCode: 'Código mágico incorrecto.',
pleaseFillTwoFactorAuth: 'Por favor, introduce tu código 2 factores',
},
},
passwordForm: {
pleaseFillOutAllFields: 'Por favor completa todos los campos',
pleaseFillPassword: 'Por favor, introduce tu contraseña',
Expand Down
9 changes: 9 additions & 0 deletions src/libs/Permissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ function canUsePolicyExpenseChat(betas) {
return _.contains(betas, CONST.BETAS.POLICY_EXPENSE_CHAT) || canUseAllBetas(betas);
}

/**
* @param {Array<String>} betas
* @returns {Boolean}
*/
function canUsePasswordlessLogins(betas) {
return _.contains(betas, CONST.BETAS.PASSWORDLESS) || canUseAllBetas(betas);
}

export default {
canUseChronos,
canUseIOU,
Expand All @@ -105,4 +113,5 @@ export default {
canUseCommentLinking,
canUsePolicyRooms,
canUsePolicyExpenseChat,
canUsePasswordlessLogins,
};
9 changes: 9 additions & 0 deletions src/libs/ValidationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,14 @@ function isValidPassword(password) {
return password.match(CONST.PASSWORD_COMPLEXITY_REGEX_STRING);
}

/**
* @param {string} validateCode
* @returns {Boolean}
*/
function isValidValidateCode(validateCode) {
return validateCode.match(CONST.VALIDATE_CODE_REGEX_STRING);
}

/**
* @param {String} code
* @returns {Boolean}
Expand Down Expand Up @@ -446,5 +454,6 @@ export {
isExistingRoomName,
isValidRoomName,
isValidTaxID,
isValidValidateCode,
findInvalidSymbols,
};
15 changes: 12 additions & 3 deletions src/libs/actions/Session/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,11 @@ function signInWithShortLivedAuthToken(email, authToken) {
* then it will create a temporary login for them which is used when re-authenticating
* after an authToken expires.
*
* @param {String} password
* @param {String} password This will be removed after passwordless beta ends
* @param {String} [validateCode] Code for passwordless login
* @param {String} [twoFactorAuthCode]
*/
function signIn(password, twoFactorAuthCode) {
function signIn(password, validateCode, twoFactorAuthCode) {
const optimisticData = [
{
onyxMethod: CONST.ONYX.METHOD.MERGE,
Expand Down Expand Up @@ -236,7 +237,15 @@ function signIn(password, twoFactorAuthCode) {
},
];

API.write('SigninUser', {email: credentials.login, password, twoFactorAuthCode}, {optimisticData, successData, failureData});
// Conditionally pass a password or validateCode to command since we temporarily allow both flows
const params = {email: credentials.login, twoFactorAuthCode};
if (validateCode) {
params.validateCode = validateCode;
} else {
params.password = password;
}

API.write('SigninUser', params, {optimisticData, successData, failureData});
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/libs/actions/User.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,10 @@ function closeAccount(message) {
* Resends a validation link to a given login
*
* @param {String} login
* @param {Boolean} isPasswordless - temporary param to trigger passwordless flow in backend
*/
function resendValidateCode(login) {
DeprecatedAPI.ResendValidateCode({email: login});
function resendValidateCode(login, isPasswordless = false) {
DeprecatedAPI.ResendValidateCode({email: login, isPasswordless});
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/pages/signin/PasswordForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ class PasswordForm extends React.Component {
formError: null,
});

Session.signIn(password, twoFactorCode);
Session.signIn(password, '', twoFactorCode);
}

render() {
Expand Down
53 changes: 43 additions & 10 deletions src/pages/signin/SignInPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ 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 Permissions from '../../libs/Permissions';

const propTypes = {
/* Onyx Props */
Expand All @@ -26,6 +28,9 @@ const propTypes = {
validated: PropTypes.bool,
}),

/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),

/** The credentials of the person signing in */
credentials: PropTypes.shape({
login: PropTypes.string,
Expand All @@ -38,6 +43,7 @@ const propTypes = {

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

Expand All @@ -51,37 +57,63 @@ class SignInPage extends Component {
// - A login has not been entered yet
const showLoginForm = !this.props.credentials.login;

// Show the password form if
// 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 user is NOT on the passwordless beta
const showPasswordForm = this.props.credentials.login
&& this.props.account.validated
&& !this.props.credentials.password
&& !this.props.account.forgotPassword;
&& !this.props.account.forgotPassword
&& !Permissions.canUsePasswordlessLogins(this.props.betas);

// Show the new magic code / validate code form if
// - A login has been entered
// - AND the user is on the 'passwordless' beta
const showValidateCodeForm = this.props.credentials.login
&& 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
const shouldShowResendValidationLinkForm = this.props.credentials.login
&& (!this.props.account.validated || this.props.account.forgotPassword);
// - AND user is not on 'passwordless' beta
const showResendValidationForm = this.props.credentials.login
&& (!this.props.account.validated || this.props.account.forgotPassword)
&& !Permissions.canUsePasswordlessLogins(this.props.betas);

const welcomeText = shouldShowResendValidationLinkForm
? ''
: this.props.translate(`welcomeText.${showPasswordForm ? 'welcomeBack' : 'welcome'}`);
let welcomeText = '';
if (showValidateCodeForm) {
if (this.props.account.requiresTwoFactorAuth) {
// We will only know this after a user signs in successfully, without their 2FA code
welcomeText = this.props.translate('validateCodeForm.enterAuthenticatorCode');
} else {
welcomeText = this.props.account.validated
? this.props.translate('welcomeText.welcomeBackEnterMagicCode', {login: this.props.credentials.login})
: this.props.translate('welcomeText.welcomeEnterMagicCode', {login: this.props.credentials.login});
}
} else if (showPasswordForm) {
welcomeText = this.props.translate('welcomeText.welcomeBack');
} else if (!showResendValidationForm) {
welcomeText = this.props.translate('welcomeText.welcome');
}

return (
<SafeAreaView style={[styles.signInPage]}>
<SignInPageLayout
welcomeText={welcomeText}
shouldShowWelcomeText={showLoginForm || showPasswordForm || !shouldShowResendValidationLinkForm}
shouldShowWelcomeText={showLoginForm || showPasswordForm || showValidateCodeForm || !showResendValidationForm}
>
{/* 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} />
<PasswordForm isVisible={showPasswordForm} />
{shouldShowResendValidationLinkForm && <ResendValidationForm />}
{showValidateCodeForm ? (
<ValidateCodeForm isVisible={showValidateCodeForm} />
) : (
<PasswordForm isVisible={showPasswordForm} />
)}
{showResendValidationForm && <ResendValidationForm />}
</SignInPageLayout>
</SafeAreaView>
);
Expand All @@ -95,6 +127,7 @@ export default compose(
withLocalize,
withOnyx({
account: {key: ONYXKEYS.ACCOUNT},
betas: {key: ONYXKEYS.BETAS},
credentials: {key: ONYXKEYS.CREDENTIALS},
}),
)(SignInPage);
Loading

0 comments on commit e1c554a

Please sign in to comment.