From eaa6ea0c434f3a4a72881fa097622dda39bdf27b Mon Sep 17 00:00:00 2001 From: Biser Perchinkov Date: Fri, 6 Jan 2017 13:01:31 +0200 Subject: [PATCH] Signup: Properly suggest non-taken usernames when signing up - Redux (#6596) --- client/components/signup-form/index.jsx | 28 +++----- client/lib/signup/step-actions.js | 70 ++++++++++++++++++- client/signup/steps/domains/index.jsx | 10 ++- client/state/action-types.js | 1 + client/state/signup/README.md | 7 ++ .../signup/optional-dependencies/reducer.js | 28 ++++++++ .../signup/optional-dependencies/schema.js | 3 + client/state/signup/reducer.js | 2 + 8 files changed, 129 insertions(+), 20 deletions(-) create mode 100644 client/state/signup/optional-dependencies/reducer.js create mode 100644 client/state/signup/optional-dependencies/schema.js diff --git a/client/components/signup-form/index.jsx b/client/components/signup-form/index.jsx index 8c585dcd7a886..cd765ef509f11 100644 --- a/client/components/signup-form/index.jsx +++ b/client/components/signup-form/index.jsx @@ -2,7 +2,7 @@ * External dependencies */ import React from 'react'; -import { map, forEach, head, includes, keys, find } from 'lodash'; +import { map, forEach, head, includes, keys } from 'lodash'; import debugModule from 'debug'; import classNames from 'classnames'; import i18n from 'i18n-calypso'; @@ -26,7 +26,7 @@ import formState from 'lib/form-state'; import LoggedOutFormLinks from 'components/logged-out-form/links'; import LoggedOutFormLinkItem from 'components/logged-out-form/link-item'; import LoggedOutFormFooter from 'components/logged-out-form/footer'; -import { getValueFromProgressStore, mergeFormWithValue, getFlowSteps } from 'signup/utils'; +import { mergeFormWithValue } from 'signup/utils'; const VALIDATION_DELAY_AFTER_FIELD_CHANGES = 1500, debug = debugModule( 'calypso:signup-form:form' ); @@ -45,6 +45,10 @@ export default React.createClass( { displayName: 'SignupForm', + contextTypes: { + store: React.PropTypes.object + }, + getInitialState() { return { notice: null, @@ -64,25 +68,13 @@ export default React.createClass( { }, autoFillUsername( form ) { - const steps = getFlowSteps( this.props.flowName ); - const domainStep = find( steps, step => step.match( /^domain/ ) ); - let domainName = getValueFromProgressStore( { - stepName: domainStep || null, - fieldName: 'siteUrl', - signupProgressStore: this.props.signupProgressStore - } ); - const siteName = getValueFromProgressStore( { - stepName: 'site', - fieldName: 'site', - signupProgressStore: this.props.signupProgressStore - } ); - if ( domainName ) { - domainName = domainName.split( '.' )[ 0 ]; - } + // Fetch the suggested username from local storage + const suggestedUsername = this.context.store.getState().signup.optionalDependencies.suggestedUsername; + return mergeFormWithValue( { form, fieldName: 'username', - fieldValue: siteName || domainName || null + fieldValue: suggestedUsername || undefined } ); }, diff --git a/client/lib/signup/step-actions.js b/client/lib/signup/step-actions.js index 40760100fd39d..22f9a7ff485fa 100644 --- a/client/lib/signup/step-actions.js +++ b/client/lib/signup/step-actions.js @@ -20,6 +20,10 @@ import { startFreeTrial } from 'lib/upgrades/actions'; import { PLAN_PREMIUM } from 'lib/plans/constants'; import analytics from 'lib/analytics'; +import { + SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET, +} from 'state/action-types'; + import { getSiteTitle } from 'state/signup/steps/site-title/selectors'; import { getSurveyVertical, getSurveySiteType } from 'state/signup/steps/survey/selectors'; @@ -152,6 +156,68 @@ function setThemeOnSite( callback, { siteSlug }, { themeSlug } ) { } ); } +/** + * Gets username suggestions from the API. + * + * Ask the API to validate a username. + * + * If the API returns a suggestion, then the username is already taken. + * If there is no error from the API, then the username is free. + * + * @param {string} username The username to get suggestions for. + * @param {object} reduxState The Redux state object + */ +function getUsernameSuggestion( username, reduxState ) { + const fields = { + givesuggestions: 1, + username: username + }; + + // Clear out the local storage variable before sending the call. + reduxState.dispatch( { + type: SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET, + data: '' + } ); + + wpcom.undocumented().validateNewUser( fields, ( error, response ) => { + if ( error || ! response ) { + return null; + } + + /** + * Default the suggested username to `username` because if the validation succeeds would mean + * that the username is free + */ + let resultingUsername = username; + + /** + * Only start checking for suggested username if the API returns an error for the validation. + */ + if ( ! response.success ) { + const { messages } = response; + + /** + * The only case we want to update username field is when the username is already taken. + * + * This ensures that the validation is done + * + * Check for: + * - username taken error - + * - a valid suggested username + */ + if ( messages.username && messages.username.taken && messages.suggested_username ) { + resultingUsername = messages.suggested_username.data; + } + } + + // Save the suggested username for later use + reduxState.dispatch( { + type: SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET, + data: resultingUsername + } ); + } ); +} + module.exports = { createSiteWithCart: createSiteWithCart, @@ -233,5 +299,7 @@ module.exports = { } ); }, - setThemeOnSite: setThemeOnSite + setThemeOnSite: setThemeOnSite, + + getUsernameSuggestion: getUsernameSuggestion }; diff --git a/client/signup/steps/domains/index.jsx b/client/signup/steps/domains/index.jsx index 093d2f8bc1280..1f55db0e5e331 100644 --- a/client/signup/steps/domains/index.jsx +++ b/client/signup/steps/domains/index.jsx @@ -19,7 +19,8 @@ var StepWrapper = require( 'signup/step-wrapper' ), { DOMAINS_WITH_PLANS_ONLY } = require( 'state/current-user/constants' ), { getSurveyVertical } = require( 'state/signup/steps/survey/selectors.js' ), analyticsMixin = require( 'lib/mixins/analytics' ), - signupUtils = require( 'signup/utils' ); + signupUtils = require( 'signup/utils' ), + getUsernameSuggestion = require( 'lib/signup/step-actions' ).getUsernameSuggestion; import { getCurrentUser, currentUserHasFlag } from 'state/current-user/selectors'; import Notice from 'components/notice'; @@ -28,6 +29,10 @@ const registerDomainAnalytics = analyticsMixin( 'registerDomain' ), mapDomainAnalytics = analyticsMixin( 'mapDomain' ); const DomainsStep = React.createClass( { + contextTypes: { + store: React.PropTypes.object + }, + showDomainSearch: function() { page( signupUtils.getStepUrl( this.props.flowName, this.props.stepName, this.props.locale ) ); }, @@ -120,6 +125,9 @@ const DomainsStep = React.createClass( { }, this.getThemeArgs() ), [], { domainItem } ); this.props.goToNextStep(); + + // Start the username suggestion process. + getUsernameSuggestion( siteUrl.split( '.' )[ 0 ], this.context.store ); }, handleAddMapping: function( sectionName, domain, state ) { diff --git a/client/state/action-types.js b/client/state/action-types.js index 21bf312e8c220..518416fb8c3d8 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -477,6 +477,7 @@ export const SHORTCODE_REQUEST_FAILURE = 'SHORTCODE_REQUEST_FAILURE'; export const SHORTCODE_REQUEST_SUCCESS = 'SHORTCODE_REQUEST_SUCCESS'; export const SIGNUP_COMPLETE_RESET = 'SIGNUP_COMPLETE_RESET'; export const SIGNUP_DEPENDENCY_STORE_UPDATE = 'SIGNUP_DEPENDENCY_STORE_UPDATE'; +export const SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET = 'SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET'; export const SIGNUP_STEPS_SITE_TITLE_SET = 'SIGNUP_STEPS_SITE_TITLE_SET'; export const SIGNUP_STEPS_SURVEY_SET = 'SIGNUP_STEPS_SURVEY_SET'; export const SITE_DOMAINS_RECEIVE = 'SITE_DOMAINS_RECEIVE'; diff --git a/client/state/signup/README.md b/client/state/signup/README.md index eae403134169a..ecf4dfa895842 100644 --- a/client/state/signup/README.md +++ b/client/state/signup/README.md @@ -15,3 +15,10 @@ It has only two reducers, because these are the only actions that actually happe * `SIGNUP_DEPENDENCY_STORE_UPDATE` - update store data with new values * `SIGNUP_COMPLETE_RESET` - clear out the store +### `optional-dependencies` + +Holds optional data that is used between the steps, outside of the defined `dependencies`, which are stored inside `SignupDependencyStore`. + +Each piece of data is defined as a separate reducer. + + * `SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET` - Username suggestion based on the chosen domain name during Signup. [PR for more information](https://github.com/Automattic/wp-calypso/pull/6596) diff --git a/client/state/signup/optional-dependencies/reducer.js b/client/state/signup/optional-dependencies/reducer.js new file mode 100644 index 0000000000000..d45a2dc23e6f5 --- /dev/null +++ b/client/state/signup/optional-dependencies/reducer.js @@ -0,0 +1,28 @@ +/** + * External dependencies + */ +import { combineReducers } from 'redux'; + +/** + * Internal dependencies + */ +import { + SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET, +} from 'state/action-types'; + +import { createReducer } from 'state/utils'; +import { suggestedUsernameSchema } from './schema'; + +const suggestedUsername = createReducer( '', + { + [ SIGNUP_OPTIONAL_DEPENDENCY_SUGGESTED_USERNAME_SET ]: ( state = null, action ) => { + return action.data; + } + }, + suggestedUsernameSchema +); + +export default combineReducers( { + suggestedUsername +} ); + diff --git a/client/state/signup/optional-dependencies/schema.js b/client/state/signup/optional-dependencies/schema.js new file mode 100644 index 0000000000000..e167e89e0ed3b --- /dev/null +++ b/client/state/signup/optional-dependencies/schema.js @@ -0,0 +1,3 @@ +export const suggestedUsername = { + type: [ 'string', 'null' ] +}; diff --git a/client/state/signup/reducer.js b/client/state/signup/reducer.js index 08844ea2b3272..eb3e738928ff6 100644 --- a/client/state/signup/reducer.js +++ b/client/state/signup/reducer.js @@ -7,9 +7,11 @@ import { combineReducers } from 'redux'; * Internal dependencies */ import dependencyStore from './dependency-store/reducer'; +import optionalDependencies from './optional-dependencies/reducer'; import steps from './steps/reducer'; export default combineReducers( { dependencyStore, + optionalDependencies, steps, } );