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

improve setPassword UX #6587

Merged
merged 11 commits into from
Jan 24, 2022
3 changes: 3 additions & 0 deletions src/ONYXKEYS.js
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,7 @@ export default {

// The policyID of the last workspace whose settings were accessed by the user
LAST_ACCESSED_WORKSPACE_POLICY_ID: 'lastAccessedWorkspacePolicyID',

// Validating Email?
USER_SIGN_UP: 'userSignUp',
};
5 changes: 3 additions & 2 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,9 @@ export default {
setPassword: 'Set password',
newPasswordPrompt: 'Your password must have at least 8 characters, 1 capital letter, 1 lowercase letter, and 1 number.',
passwordFormTitle: 'Welcome back to the New Expensify! Please set your password.',
passwordNotSet: 'We were unable to set your new password correctly.',
accountNotValidated: 'We were unable to validate your account. The validation code may have expired.',
passwordNotSet: 'We were unable to set your new password. We have sent you a new password link to try again.',
setPasswordLinkInvalid: 'This set password link is invalid or has expired. A new one is waiting for you in your email inbox!',
verifyingAccount: 'Verifying account',
},
stepCounter: ({step, total}) => `Step ${step} of ${total}`,
bankAccount: {
Expand Down
5 changes: 3 additions & 2 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -464,8 +464,9 @@ export default {
setPassword: 'Configura tu contraseña',
newPasswordPrompt: 'La contraseña debe tener al menos 8 caracteres, 1 letra mayúscula, 1 letra minúscula y 1 número.',
passwordFormTitle: '¡Bienvenido de vuelta al Nuevo Expensify! Por favor, elige una contraseña.',
passwordNotSet: 'No pudimos establecer to contaseña correctamente.',
accountNotValidated: 'No pudimos validar tu cuenta. Es posible que el enlace de validación haya caducado.',
passwordNotSet: 'No pudimos cambiar tu clave. Te hemos enviado un nuevo enlace para que intentes cambiar la clave nuevamente.',
setPasswordLinkInvalid: 'El enlace para configurar tu contraseña ha expirado. Te hemos enviado un nuevo enlace a tu correo.',
verifyingAccount: 'Verificando cuenta',
},
stepCounter: ({step, total}) => `Paso ${step} de ${total}`,
bankAccount: {
Expand Down
8 changes: 4 additions & 4 deletions src/libs/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ function AddBillingCard(parameters) {


/**
* @param {Object} parameters
* @param {String} parameters.oldPassword
* @param {{password: String, oldPassword: String}} parameters
* @param {String} parameters.authToken
* @param {String} parameters.password
* @returns {Promise}
*/
Expand Down Expand Up @@ -717,7 +717,7 @@ function SetNameValuePair(parameters) {

/**
* @param {Object} parameters
* @param {Number} parameters.email
* @param {string} parameters.email
* @returns {Promise}
*/
function ResetPassword(parameters) {
Expand All @@ -730,7 +730,7 @@ function ResetPassword(parameters) {
* @param {Object} parameters
* @param {String} parameters.password
* @param {String} parameters.validateCode
* @param {String} parameters.accountID
* @param {Number} parameters.accountID
* @returns {Promise}
*/
function SetPassword(parameters) {
Expand Down
69 changes: 51 additions & 18 deletions src/libs/actions/Session/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -293,11 +293,10 @@ function resetPassword() {
*
* @param {String} password
* @param {String} validateCode
* @param {String} accountID
* @param {Number} accountID
*/
function setPassword(password, validateCode, accountID) {
Onyx.merge(ONYXKEYS.ACCOUNT, {...CONST.DEFAULT_ACCOUNT_DATA, loading: true, validateCodeExpired: false});

API.SetPassword({
password,
validateCode,
Expand Down Expand Up @@ -384,52 +383,84 @@ function clearAccountMessages() {
}

/**
* Calls change password and signs if if successful. Otherwise, we request a new magic link
* if we know the account email. Otherwise or finally we redirect to the root of the nav.
* @param {String} authToken
* @param {String} password
*/
function changePasswordAndSignIn(authToken, password) {
Onyx.merge(ONYXKEYS.ACCOUNT, {validateSessionExpired: false});
API.ChangePassword({
authToken,
password,
})
.then((responsePassword) => {
Onyx.merge(ONYXKEYS.USER_SIGN_UP, {authToken: null});
if (responsePassword.jsonCode === 200) {
signIn(password);
return;
}

if (responsePassword.jsonCode === 407 && !credentials.login) {
// authToken has expired, and we don't have the email set to request a new magic link.
// send user to login page to enter email.
Navigation.navigate(ROUTES.HOME);
return;
}
if (responsePassword.jsonCode === 407) {
// authToken has expired, and we have the account email, so we request a new magic link.
Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validateCodeExpired: true, error: null});
resetPassword();
Navigation.navigate(ROUTES.HOME);
return;
}
Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.passwordNotSet'});
});
}

/**
* @param {String} accountID
* Call set or change password based on if we have an auth token
* @param {Number} accountID
* @param {String} validateCode
* @param {String} password
* @param {String} authToken
*/
function validateEmail(accountID, validateCode, password) {

function setOrChangePassword(accountID, validateCode, password, authToken) {
if (authToken) {
changePasswordAndSignIn(authToken, password);
return;
}
setPassword(password, validateCode, accountID);
}

/**
* @param {Number} accountID
* @param {String} validateCode
* @param {String} login
* @param {String} authToken
*/
function validateEmail(accountID, validateCode) {
Onyx.merge(ONYXKEYS.USER_SIGN_UP, {isValidating: true});
Onyx.merge(ONYXKEYS.SESSION, {error: ''});
API.ValidateEmail({
accountID,
validateCode,
})
.then((responseValidate) => {
if (responseValidate.jsonCode === 200) {
changePasswordAndSignIn(responseValidate.authToken, password);
Onyx.merge(ONYXKEYS.USER_SIGN_UP, {authToken: responseValidate.authToken});
Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validated: true});
Onyx.merge(ONYXKEYS.CREDENTIALS, {login: responseValidate.email});
return;
}

if (responseValidate.title === CONST.PASSWORD_PAGE.ERROR.ALREADY_VALIDATED) {
// If the email is already validated, set the password using the validate code
setPassword(
password,
validateCode,
accountID,
);
return;
if (responseValidate.jsonCode === 666) {
Onyx.merge(ONYXKEYS.ACCOUNT, {accountExists: true, validated: true});
}

Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.accountNotValidated'});
});
if (responseValidate.jsonCode === 401) {
Onyx.merge(ONYXKEYS.SESSION, {error: 'setPasswordPage.setPasswordLinkInvalid'});
}
})
.finally(Onyx.merge(ONYXKEYS.USER_SIGN_UP, {isValidating: false}));
}

// It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to
Expand Down Expand Up @@ -493,6 +524,7 @@ function setShouldShowComposeInput(shouldShowComposeInput) {
export {
continueSessionFromECom,
fetchAccountDetails,
setOrChangePassword,
setPassword,
signIn,
signInWithShortLivedToken,
Expand All @@ -507,4 +539,5 @@ export {
authenticatePusher,
reauthenticatePusher,
setShouldShowComposeInput,
changePasswordAndSignIn,
};
82 changes: 52 additions & 30 deletions src/pages/SetPasswordPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const propTypes = {
/** An error message to display to the user */
error: PropTypes.string,

/** Whether or not a sign on form is loading (being submitted) */
/** Whether a sign on form is loading (being submitted) */
loading: PropTypes.bool,
}),

Expand All @@ -48,6 +48,15 @@ const propTypes = {
error: PropTypes.string,
}),

/** User signup object */
userSignUp: PropTypes.shape({
/** Is Validating Email */
isValidating: PropTypes.bool,

/** Auth token used to change password */
authToken: PropTypes.string,
}),

/** The accountID and validateCode are passed via the URL */
route: validateLinkPropTypes,

Expand All @@ -60,6 +69,11 @@ const defaultProps = {
route: validateLinkDefaultProps,
session: {
error: '',
authToken: '',
},
userSignUp: {
isValidating: false,
authToken: '',
},
};

Expand All @@ -75,20 +89,27 @@ class SetPasswordPage extends Component {
};
}

/**
* Validate the form and then submit it
*/
validateAndSubmitForm() {
componentDidMount() {
const accountID = lodashGet(this.props.route.params, 'accountID', '');
const validateCode = lodashGet(this.props.route.params, 'validateCode', '');
if (!this.state.isFormValid) {
if (this.props.userSignUp.authToken) {
return;
}
Session.validateEmail(accountID, validateCode);
}

Session.validateEmail(accountID, validateCode, this.state.password);
validateAndSubmitForm() {
if (!this.state.isFormValid) {
return;
}
const accountID = lodashGet(this.props.route.params, 'accountID', '');
const validateCode = lodashGet(this.props.route.params, 'validateCode', '');
Session.setOrChangePassword(accountID, validateCode, this.state.password, this.props.userSignUp.authToken);
}


render() {
const buttonText = this.props.userSignUp.isValidating ? this.props.translate('setPasswordPage.verifyingAccount') : this.props.translate('setPasswordPage.setPassword');
const sessionError = this.props.session.error && this.props.translate(this.props.session.error);
const error = sessionError || this.props.account.error;
return (
Expand All @@ -97,29 +118,29 @@ class SetPasswordPage extends Component {
shouldShowWelcomeText
welcomeText={this.props.translate('setPasswordPage.passwordFormTitle')}
>
<View style={[styles.mb4]}>
<NewPasswordForm
password={this.state.password}
updatePassword={password => this.setState({password})}
updateIsFormValid={isValid => this.setState({isFormValid: isValid})}
onSubmitEditing={this.validateAndSubmitForm}
/>
</View>
<View>
<Button
success
style={[styles.mb2]}
text={this.props.translate('setPasswordPage.setPassword')}
isLoading={this.props.account.loading}
onPress={this.validateAndSubmitForm}
isDisabled={!this.state.isFormValid}
/>
</View>
{!_.isEmpty(error) && (
<Text style={[styles.formError]}>
{error}
</Text>
)}
{_.isEmpty(error) ? (
<>
<View style={[styles.mb4]}>
<NewPasswordForm
password={this.state.password}
updatePassword={password => this.setState({password})}
updateIsFormValid={isValid => this.setState({isFormValid: isValid})}
onSubmitEditing={this.validateAndSubmitForm}
/>
</View>
<View>
<Button
success
style={[styles.mb2]}
text={buttonText}
isLoading={this.props.account.loading || this.props.userSignUp.isValidatingEmail}
onPress={this.validateAndSubmitForm}
isDisabled={!this.state.isFormValid}
/>
</View>
</>
)
: (<Text>{error}</Text>)}
</SignInPageLayout>
</SafeAreaView>
);
Expand All @@ -138,5 +159,6 @@ export default compose(
key: ONYXKEYS.SESSION,
initWithStoredValues: false,
},
userSignUp: {key: ONYXKEYS.USER_SIGN_UP},
}),
)(SetPasswordPage);