packages/fxa-content-server/app/scripts/views/complete_sign_up.js (138 lines of code) (raw):
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Complete sign up is used to complete the email verification for
* multiple types of users:
*
* 1. New users that just signed up.
* 2. Existing users that have signed in with an unverified account.
* 3. Existing users that are signing into Sync and
* must re-confirm their account.
* 4. Existing users that confirmed a secondary email.
*
* The auth server endpoints that are called are the same in all cases.
*/
import AuthErrors from '../lib/auth-errors';
import BaseView from './base';
import Cocktail from 'cocktail';
import CompleteSignUpTemplate from 'templates/complete_sign_up.mustache';
import ConnectAnotherDeviceMixin from './mixins/connect-another-device-mixin';
import ResendMixin from './mixins/resend-mixin';
import ResumeTokenMixin from './mixins/resume-token-mixin';
import VerificationInfo from '../models/verification/sign-up';
const CompleteSignUpView = BaseView.extend({
template: CompleteSignUpTemplate,
className: 'complete_sign_up',
initialize(options = {}) {
this._verificationInfo = new VerificationInfo(this.getSearchParams());
const uid = this._verificationInfo.get('uid');
this.notifier.trigger('set-uid', uid);
const account = options.account || this.user.getAccountByUid(uid);
// the account will not exist if verifying in a second browser, and the
// default account will be returned. Add the uid to the account so
// verification can still occur.
if (account.isDefault()) {
account.set('uid', uid);
}
this._account = account;
// cache the email in case we need to attempt to resend the
// verification link
this._email = this._account.get('email');
},
getAccount() {
return this._account;
},
beforeRender() {
this.logViewEvent('verification.clicked');
const verificationInfo = this._verificationInfo;
if (!verificationInfo.isValid()) {
// One or more parameters fails validation. Abort and show an
// error message before doing any more checks.
this.logError(AuthErrors.toError('DAMAGED_VERIFICATION_LINK'));
return true;
}
const account = this.getAccount();
// Loads the email from the resume token to smooth out the signin
// flow if the user verifies in a 2nd Firefox.
account.populateFromStringifiedResumeToken(this.getSearchParam('resume'));
const code = verificationInfo.get('code');
const options = {
primaryEmailVerified:
this.getSearchParam('primary_email_verified') || null,
reminder: verificationInfo.get('reminder'),
secondaryEmailVerified:
this.getSearchParam('secondary_email_verified') || null,
service: this.relier.get('service') || null,
style: this.relier.get('style') || null,
type: verificationInfo.get('type'),
};
return this.user
.completeAccountSignUp(account, code, options)
.then(() => this._notifyBrokerAndComplete(account))
.catch((err) => this._handleVerificationErrors(err));
},
setInitialContext(context) {
const verificationInfo = this._verificationInfo;
context.set({
canResend: this._canResend(),
error: this.model.get('error'),
// If the link is invalid, print a special error message.
isLinkDamaged: !verificationInfo.isValid(),
isLinkExpired: verificationInfo.isExpired(),
isLinkUsed: verificationInfo.isUsed(),
isPrimaryEmailVerification: this.isPrimaryEmail(),
});
},
/**
* Notify the broker that the email is verified. Brokers are
* expected to take care of any next steps.
*
* @param {Object} account
* @returns {Promise}
* @private
*/
_notifyBrokerAndComplete(account) {
this.logViewEvent('verification.success');
this.notifier.trigger('verification.success');
// Emitting an explicit signin event here
// allows us to capture successes that might be
// triggered from confirmation emails.
if (this.isSignIn()) {
this.logEvent('signin.success');
}
const brokerMethod = this._getBrokerMethod();
// The brokers handle all next steps.
return this.invokeBrokerMethod(brokerMethod, account);
},
/**
* Get the post-verification broker method name.
*
* @returns {String}
* @throws Error if suitable broker method is not available.
*/
_getBrokerMethod() {
let brokerMethod;
if (this.isPrimaryEmail()) {
brokerMethod = 'afterCompletePrimaryEmail';
} else if (this.isSecondaryEmail()) {
brokerMethod = 'afterCompleteSecondaryEmail';
} else if (this.isSignIn()) {
brokerMethod = 'afterCompleteSignIn';
} else if (this.isSignUp()) {
brokerMethod = 'afterCompleteSignUp';
} else {
throw new Error(`New broker method needed for ${this.model.get('type')}`);
}
return brokerMethod;
},
/**
* Handle any verification errors.
*
* @param {Error} err
* @private
*/
_handleVerificationErrors(err) {
const verificationInfo = this._verificationInfo;
if (AuthErrors.is(err, 'UNKNOWN_ACCOUNT')) {
verificationInfo.markExpired();
err = AuthErrors.toError('UNKNOWN_ACCOUNT_VERIFICATION');
} else if (
AuthErrors.is(err, 'INVALID_VERIFICATION_CODE') ||
AuthErrors.is(err, 'INVALID_PARAMETER')
) {
if (this.isPrimaryEmail()) {
verificationInfo.markUsed();
err = AuthErrors.toError('REUSED_PRIMARY_EMAIL_VERIFICATION_CODE');
} else if (this.isSignIn()) {
// When coming from sign-in confirmation verification, show a
// verification link expired error instead of damaged verification link.
// This error is generated because the link has already been used.
//
// Disable resending verification, can only be triggered from new sign-in
verificationInfo.markUsed();
err = AuthErrors.toError('REUSED_SIGNIN_VERIFICATION_CODE');
} else {
// These server says the verification code or any parameter is
// invalid. The entire link is damaged.
verificationInfo.markDamaged();
err = AuthErrors.toError('DAMAGED_VERIFICATION_LINK');
}
} else {
// all other errors show the standard error box.
this.model.set('error', err);
}
this.logError(err);
},
/**
* Check whether the user can resend a signup verification email to allow
* users to recover from expired verification links.
*
* @returns {Boolean}
* @private
*/
_canResend() {
// _hasResendSessionToken only returns `true` if the user signed up in the
// same browser in which they opened the verification link.
return !!this._hasResendSessionToken() && this.isSignUp();
},
/**
* Returns whether a sessionToken exists for the user's email.
* The sessionToken is not cached during view initialization so that
* we can capture sessionTokens from accounts created (in this browser)
* since the view was loaded.
*
* @returns {Boolean}
* @private
*/
_hasResendSessionToken() {
return !!this.user.getAccountByEmail(this._email).get('sessionToken');
},
/**
* Resend a signup verification link to the user. Called when a
* user follows an expired verification link and clicks "resend"
*
* @returns {Promise}
*/
resend() {
const account = this.user.getAccountByEmail(this._email);
return account
.retrySignUp(this.relier, {
resume: this.getStringifiedResumeToken(account),
})
.catch((err) => {
if (AuthErrors.is(err, 'INVALID_TOKEN')) {
return this.navigate('signup', {
error: err,
});
}
// unexpected error, rethrow for display.
throw err;
});
},
});
Cocktail.mixin(
CompleteSignUpView,
ConnectAnotherDeviceMixin,
ResendMixin(),
ResumeTokenMixin
);
export default CompleteSignUpView;