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

Add conditional passwordless flow #13655

Merged
merged 38 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d7a5369
TODOs in signin page
Beamanator Dec 16, 2022
1bdbe88
A few magic code translations
Beamanator Dec 16, 2022
19033be
Passwordless betas
Beamanator Dec 16, 2022
8822080
Initial new validate code form & action
Beamanator Dec 16, 2022
145c457
Pass betas into SignInPage
Beamanator Dec 20, 2022
bb5b663
Cleanup to use passwordless permission check
Beamanator Dec 20, 2022
25d4b9d
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Dec 20, 2022
0208f86
New magic code / 2fa translations
Beamanator Dec 20, 2022
581f951
New validate code validation function
Beamanator Dec 20, 2022
aa6108d
Rename send validate code form
Beamanator Dec 20, 2022
8579cfd
Update to use validateCodes & new UI
Beamanator Dec 20, 2022
40f9fc0
Update sign in page with new form and text
Beamanator Dec 20, 2022
0ea78ed
Remove unused error
Beamanator Dec 20, 2022
52355cf
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Dec 21, 2022
13ff2b7
Remove sendValidateCode since it doesn't exist yet
Beamanator Dec 21, 2022
119db2c
Add validation back, use resendValidateCode
Beamanator Dec 21, 2022
af94044
Get login to pass to ResendValidateCode
Beamanator Dec 21, 2022
9e474ce
Make welcomeText logic more clear
Beamanator Dec 21, 2022
3cccdd4
Pass password or validateCode to SigninUser
Beamanator Dec 21, 2022
14eb2e3
Lint fixes
Beamanator Dec 21, 2022
2ddee7b
Adapt signin, not sure what to do with accountID yet
Beamanator Dec 21, 2022
35d729c
Add missing spanish translations
Beamanator Dec 21, 2022
96017a1
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 6, 2023
ce91caa
Change ResendValidationForm name back
Beamanator Jan 6, 2023
100b72b
Condense comment
Beamanator Jan 6, 2023
d777db3
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 10, 2023
f6420ed
Using informal lang (es)
Beamanator Jan 10, 2023
ba95d92
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 19, 2023
af761cd
Revert changes to this page
Beamanator Jan 19, 2023
d22df59
Resolve lint error
Beamanator Jan 19, 2023
4600cc4
Remove comment causing lint issues
Beamanator Jan 19, 2023
f8c964a
Send isPasswordless param to resend validate code flow
Beamanator Jan 20, 2023
aba7bef
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 24, 2023
3347bf8
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 25, 2023
14eb86e
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 30, 2023
8054dc3
Merge branch 'main' of github.com:Expensify/App into beaman-addCondit…
Beamanator Jan 31, 2023
c66879b
A few PR review comments
Beamanator Jan 31, 2023
b67c710
Update 2fa error message
Beamanator Jan 31, 2023
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: 4 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const CONST = {
IOU_SEND: 'sendMoney',
POLICY_ROOMS: 'policyRooms',
POLICY_EXPENSE_CHAT: 'policyExpenseChat',
PASSWORDLESS: 'passwordless',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down Expand Up @@ -442,6 +443,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
17 changes: 16 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 @@ -159,10 +160,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 @@ -513,6 +516,18 @@ 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?",
fillTwoFactorAuthOrRecoveryCode: 'Please enter your two factor code or recovery code',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also from the figma mockups (here) this should just be Please enter your two factor code since we're trying to limit the number of "code" references we're using

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use recovery code if they enter the incorrect 2FA code with an error message like so (on oldDot):

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh I wonder if Figma was updated recently ish? I the original text here used to be accurate 😅 Hope I'm not crazy 🤞

And now it looks like the text should actually be Please enter your authenticator code to match

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requested Spanish translation here

enterTwoFactorOrRecoveryCode: 'Enter your two factor code or recovery code',
twoFactorCode: 'Two factor code',
requiredWhen2FAEnabled: 'Required when 2FA is enabled',
error: {
pleaseFillMagicCode: 'Please enter your magic code',
incorrectMagicCode: 'Incorrect magic code. Please try again.',
Beamanator marked this conversation as resolved.
Show resolved Hide resolved
pleaseFillTwoFactorAuth: 'Please enter your two factor code',
},
},
passwordForm: {
pleaseFillOutAllFields: 'Please fill out all fields',
pleaseFillPassword: 'Please enter your password',
Expand Down
15 changes: 15 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 @@ -160,9 +161,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 ingrese 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, introduzca el código mágico enviado a ${login}`,
},
reportActionCompose: {
addAction: 'Acción',
Expand Down Expand Up @@ -513,6 +516,18 @@ 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?',
fillTwoFactorAuthOrRecoveryCode: 'Introduzca el código de dos factores o código de recuperación',
enterTwoFactorOrRecoveryCode: 'Introduzca el código de dos factores o código de recuperación',
twoFactorCode: 'Autenticación de 2 factores',
requiredWhen2FAEnabled: 'Obligatorio cuando A2F está habilitado',
error: {
pleaseFillMagicCode: 'Por favor, introduzca el código mágico',
incorrectMagicCode: 'Código mágico incorrecto. Inténtalo de nuevo.',
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 @@ -274,6 +274,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 @@ -443,5 +451,6 @@ export {
isExistingRoomName,
isValidRoomName,
isValidTaxID,
isValidValidateCode,
findInvalidSymbols,
};
16 changes: 13 additions & 3 deletions src/libs/actions/Session/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,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 @@ -222,7 +223,16 @@ 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
// will 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});
Beamanator marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down
5 changes: 3 additions & 2 deletions src/pages/ValidateLoginPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import {
propTypes as validateLinkPropTypes,
defaultProps as validateLinkDefaultProps,
} from './validateLinkPropTypes';
import * as User from '../libs/actions/User';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import * as Session from '../libs/actions/Session';

const propTypes = {
/** The accountID and validateCode are passed via the URL */
Expand All @@ -17,10 +17,11 @@ const defaultProps = {
};
class ValidateLoginPage extends Component {
componentDidMount() {
// TODO: figure out if accountID is needed anymore
const accountID = lodashGet(this.props.route.params, 'accountID', '');
Beamanator marked this conversation as resolved.
Show resolved Hide resolved
const validateCode = lodashGet(this.props.route.params, 'validateCode', '');

User.validateLogin(accountID, validateCode);
Session.signIn('', validateCode);
}

render() {
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
60 changes: 49 additions & 11 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 ResendValidationForm from './ResendValidationForm';
import ValidateCodeForm from './ValidateCodeForm';
import SendValidateCodeForm from './SendValidateCodeForm';
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,68 @@ 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 showSendValidateCodeForm = 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.enterTwoFactorOrRecoveryCode');
} else if (this.props.account.validated) {
welcomeText = this.props.translate('welcomeText.welcomeBackEnterMagicCode', {login: this.props.credentials.login});
} else if (!this.props.account.validated) {
welcomeText = this.props.translate('welcomeText.welcomeEnterMagicCode', {login: this.props.credentials.login});
}
} else {
// All of this will go away when we get rid of password flows
if (showSendValidateCodeForm) {
welcomeText = '';
} else if (showPasswordForm) {
welcomeText = this.props.translate('welcomeText.welcomeBack');
} else {
welcomeText = this.props.translate('welcomeText.welcome');
}
}

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