diff --git a/client/blocks/app-promo/index.jsx b/client/blocks/app-promo/index.jsx index 34cb52a885344..e1fabc1633242 100644 --- a/client/blocks/app-promo/index.jsx +++ b/client/blocks/app-promo/index.jsx @@ -14,10 +14,10 @@ import Gridicon from 'components/gridicon'; */ import { localize } from 'i18n-calypso'; import { recordTracksEvent } from 'state/analytics/actions'; -import wpcom from 'lib/wp'; import { Dialog } from '@automattic/components'; import { fetchUserSettings } from 'state/user-settings/actions'; import getUserSettings from 'state/selectors/get-user-settings'; +import { sendEmailLogin } from 'state/auth/actions'; /** * Image dependencies @@ -110,16 +110,9 @@ export class AppPromo extends React.Component { sendMagicLink = () => { this.recordClickEvent(); - const email = this.props.userSettings.user_email; - wpcom.undocumented().requestMagicLoginEmail( { - email, - infer: true, - scheme: 'wordpress', - } ); - + this.props.sendEmailLogin( email, { showGlobalNotices: false, isMobileAppLogin: true } ); this.onShowDialog(); - return false; }; @@ -230,5 +223,5 @@ export default connect( state => ( { userSettings: getUserSettings( state ), } ), - { fetchUserSettings, recordTracksEvent } + { fetchUserSettings, recordTracksEvent, sendEmailLogin } )( localize( AppPromo ) ); diff --git a/client/blocks/get-apps/mobile-download-card.jsx b/client/blocks/get-apps/mobile-download-card.jsx index 1b267d321550b..b32a3f4bee95b 100644 --- a/client/blocks/get-apps/mobile-download-card.jsx +++ b/client/blocks/get-apps/mobile-download-card.jsx @@ -16,7 +16,7 @@ import { Card, Button } from '@automattic/components'; import QuerySmsCountries from 'components/data/query-countries/sms'; import FormPhoneInput from 'components/forms/form-phone-input'; import getCountries from 'state/selectors/get-countries'; -import { infoNotice, successNotice, errorNotice } from 'state/notices/actions'; +import { successNotice, errorNotice } from 'state/notices/actions'; import { fetchUserSettings } from 'state/user-settings/actions'; import { accountRecoverySettingsFetch } from 'state/account-recovery/settings/actions'; import { @@ -26,11 +26,11 @@ import { import getUserSettings from 'state/selectors/get-user-settings'; import hasUserSettings from 'state/selectors/has-user-settings'; import { http } from 'state/data-layer/wpcom-http/actions'; -import wpcom from 'lib/wp'; import phoneValidation from 'lib/phone-validation'; import userAgent from 'lib/user-agent'; import twoStepAuthorization from 'lib/two-step-authorization'; -import { recordTracksEvent } from 'state/analytics/actions'; +import { recordTracksEvent, withAnalytics } from 'state/analytics/actions'; +import { sendEmailLogin } from 'state/auth/actions'; function sendSMS( phone ) { function onSuccess( dispatch ) { @@ -53,30 +53,6 @@ function sendSMS( phone ) { } ); } -function sendMagicLink( email ) { - //Actions must be plain objects. Use custom middleware for async actions. - // https://stackoverflow.com/questions/46765896/react-redux-actions-must-be-plain-objects-use-custom-middleware-for-async-acti - return function( dispatch ) { - const duration = { duration: 4000 }; - dispatch( infoNotice( i18n.translate( 'Sending email' ), duration ) ); - - return wpcom - .undocumented() - .requestMagicLoginEmail( { - email, - infer: true, - scheme: 'wordpress', - } ) - .then( () => { - dispatch( successNotice( i18n.translate( 'Email Sent. Check your mail app!' ), duration ) ); - } ) - .catch( error => { - dispatch( errorNotice( i18n.translate( 'Sorry, we couldn’t send the email.' ), duration ) ); - return Promise.reject( error ); - } ); - }; -} - class MobileDownloadCard extends React.Component { static propTypes = { translate: PropTypes.func, @@ -316,12 +292,17 @@ class MobileDownloadCard extends React.Component { }; onSubmitLink = () => { - this.props.recordTracksEvent( 'calypso_get_apps_magic_link_button_click' ); const email = this.props.userSettings.user_email; this.props.sendMagicLink( email ); }; } +const sendMagicLink = email => + withAnalytics( + recordTracksEvent( 'calypso_get_apps_magic_link_button_click' ), + sendEmailLogin( email, { showGlobalNotices: true, isMobileAppLogin: true } ) + ); + export default connect( state => ( { countriesList: getCountries( state, 'sms' ), diff --git a/client/blocks/login/login-form.jsx b/client/blocks/login/login-form.jsx index ef1e3d97ede43..4db2eb213c3cd 100644 --- a/client/blocks/login/login-form.jsx +++ b/client/blocks/login/login-form.jsx @@ -20,7 +20,6 @@ import config from 'config'; import FormsButton from 'components/forms/form-button'; import FormInputValidation from 'components/forms/form-input-validation'; import Divider from './divider'; -import { fetchMagicLoginRequestEmail } from 'state/login/magic-login/actions'; import FormPasswordInput from 'components/forms/form-password-input'; import FormTextInput from 'components/forms/form-text-input'; import getCurrentQueryArguments from 'state/selectors/get-current-query-arguments'; @@ -52,12 +51,13 @@ import Notice from 'components/notice'; import SocialLoginForm from './social'; import { localizeUrl } from 'lib/i18n-utils'; import TextControl from 'extensions/woocommerce/components/text-control'; +import { sendEmailLogin } from 'state/auth/actions'; export class LoginForm extends Component { static propTypes = { accountType: PropTypes.string, disableAutoFocus: PropTypes.bool, - fetchMagicLoginRequestEmail: PropTypes.func.isRequired, + sendEmailLogin: PropTypes.func.isRequired, formUpdate: PropTypes.func.isRequired, getAuthAccountType: PropTypes.func.isRequired, hasAccountTypeLoaded: PropTypes.bool.isRequired, @@ -131,19 +131,10 @@ export class LoginForm extends Component { } if ( ! this.props.hasAccountTypeLoaded && isPasswordlessAccount( nextProps.accountType ) ) { - this.props.recordTracksEvent( 'calypso_login_block_login_form_send_magic_link' ); - - this.props - .fetchMagicLoginRequestEmail( this.state.usernameOrEmail, nextProps.redirectTo ) - .then( () => { - this.props.recordTracksEvent( 'calypso_login_block_login_form_send_magic_link_success' ); - } ) - .catch( error => { - this.props.recordTracksEvent( 'calypso_login_block_login_form_send_magic_link_failure', { - error_code: error.error, - error_message: error.message, - } ); - } ); + this.props.sendEmailLogin( this.state.usernameOrEmail, { + redirectTo: nextProps.redirectTo, + loginFormFlow: true, + } ); page( login( { isNative: true, twoFactorAuthType: 'link' } ) ); } @@ -638,7 +629,7 @@ export default connect( }; }, { - fetchMagicLoginRequestEmail, + sendEmailLogin, formUpdate, getAuthAccountType, loginUser, diff --git a/client/lib/wpcom-undocumented/lib/undocumented.js b/client/lib/wpcom-undocumented/lib/undocumented.js index 2238c851be07b..b25c884b9575a 100644 --- a/client/lib/wpcom-undocumented/lib/undocumented.js +++ b/client/lib/wpcom-undocumented/lib/undocumented.js @@ -1430,29 +1430,6 @@ Undocumented.prototype.usersEmailVerification = function( query, fn ) { return this.wpcom.req.post( args, fn ); }; -/** - * Request a "Magic Login" email be sent to a user so they can use it to log in - * - * @param {object} data - object containing an email address - * @param {Function} fn - Function to invoke when request is complete - * @returns {Promise} promise - */ -Undocumented.prototype.requestMagicLoginEmail = function( data, fn ) { - restrictByOauthKeys( data ); - - data.locale = getLocaleSlug(); - data.lang_id = getLanguage( data.locale ).value; - - return this.wpcom.req.post( - '/auth/send-login-email', - { - apiVersion: '1.2', - }, - data, - fn - ); -}; - /** * Create a new site * @@ -2142,9 +2119,9 @@ Undocumented.prototype.exportReaderFeed = function( fn ) { * Imports given XML file into the user's Reader feed. * XML file is expected to be in OPML format. * - * @param {File} file The File object to upload + * @param {globalThis.File} file The File object to upload * @param {Function} fn The callback function - * @returns {XMLHttpRequest} The XHR instance, to attach `progress` + * @returns {globalThis.XMLHttpRequest} The XHR instance, to attach `progress` * listeners to, etc. */ Undocumented.prototype.importReaderFeed = function( file, fn ) { @@ -2168,7 +2145,7 @@ Undocumented.prototype.importReaderFeed = function( file, fn ) { * @param {string} deviceFamily The device family * @param {string} deviceName The device name * @param {Function} fn The callback function - * @returns {XMLHttpRequest} The XHR instance + * @returns {globalThis.XMLHttpRequest} The XHR instance */ Undocumented.prototype.registerDevice = function( registration, deviceFamily, deviceName, fn ) { debug( '/devices/new' ); @@ -2189,7 +2166,7 @@ Undocumented.prototype.registerDevice = function( registration, deviceFamily, de * * @param {number} deviceId The device ID for the registration to be removed * @param {Function} fn The callback function - * @returns {XMLHttpRequest} The XHR instance + * @returns {globalThis.XMLHttpRequest} The XHR instance */ Undocumented.prototype.unregisterDevice = function( deviceId, fn ) { debug( '/devices/:device_id/delete' ); @@ -2213,7 +2190,7 @@ Undocumented.prototype.wordAdsApprove = function( siteId ) { * * @param {number} siteId -- the ID of the site * @param {string} [plugin] -- .org plugin slug - * @param {File} [theme] -- theme zip to upload + * @param {globalThis.File} [theme] -- theme zip to upload * @param {Function} [onProgress] -- called with upload progress status * * @returns {Promise} promise for handling result diff --git a/client/login/magic-login/request-login-email-form.jsx b/client/login/magic-login/request-login-email-form.jsx index e5b92e6bc2d9a..1ed797fd9ae0d 100644 --- a/client/login/magic-login/request-login-email-form.jsx +++ b/client/login/magic-login/request-login-email-form.jsx @@ -10,10 +10,7 @@ import { defer, get } from 'lodash'; /** * Internal dependencies */ -import { - fetchMagicLoginRequestEmail, - hideMagicLoginRequestNotice, -} from 'state/login/magic-login/actions'; +import { hideMagicLoginRequestNotice } from 'state/login/magic-login/actions'; import getMagicLoginCurrentView from 'state/selectors/get-magic-login-current-view'; import getMagicLoginRequestedEmailSuccessfully from 'state/selectors/get-magic-login-requested-email-successfully'; import getMagicLoginRequestEmailError from 'state/selectors/get-magic-login-request-email-error'; @@ -31,6 +28,7 @@ import LoggedOutForm from 'components/logged-out-form'; import Notice from 'components/notice'; import { localize } from 'i18n-calypso'; import { getCurrentUser } from 'state/current-user/selectors'; +import { sendEmailLogin } from 'state/auth/actions'; class RequestLoginEmailForm extends React.Component { static propTypes = { @@ -44,7 +42,7 @@ class RequestLoginEmailForm extends React.Component { userEmail: PropTypes.string, // mapped to dispatch - fetchMagicLoginRequestEmail: PropTypes.func.isRequired, + sendEmailLogin: PropTypes.func.isRequired, hideMagicLoginRequestNotice: PropTypes.func.isRequired, }; @@ -85,19 +83,10 @@ class RequestLoginEmailForm extends React.Component { return; } - this.props.recordTracksEvent( 'calypso_login_email_link_submit' ); - - this.props - .fetchMagicLoginRequestEmail( usernameOrEmail, this.props.redirectTo ) - .then( () => { - this.props.recordTracksEvent( 'calypso_login_email_link_success' ); - } ) - .catch( error => { - this.props.recordTracksEvent( 'calypso_login_email_link_failure', { - error_code: error.error, - error_message: error.message, - } ); - } ); + this.props.sendEmailLogin( usernameOrEmail, { + redirectTo: this.props.redirectTo, + requestLoginEmailFormFlow: true, + } ); }; getUsernameOrEmailFromState() { @@ -200,7 +189,7 @@ const mapState = state => { }; const mapDispatch = { - fetchMagicLoginRequestEmail, + sendEmailLogin, hideMagicLoginRequestNotice, recordTracksEvent, }; diff --git a/client/state/action-types.js b/client/state/action-types.js index b7454ca4bb7f7..793c3941e3250 100644 --- a/client/state/action-types.js +++ b/client/state/action-types.js @@ -526,6 +526,7 @@ export const LOGIN_AUTH_ACCOUNT_TYPE_REQUESTING = 'LOGIN_AUTH_ACCOUNT_TYPE_REQUE export const LOGIN_AUTH_ACCOUNT_TYPE_REQUEST_FAILURE = 'LOGIN_AUTH_ACCOUNT_TYPE_REQUEST_FAILURE'; export const LOGIN_AUTH_ACCOUNT_TYPE_REQUEST_SUCCESS = 'LOGIN_AUTH_ACCOUNT_TYPE_REQUEST_SUCCESS'; export const LOGIN_AUTH_ACCOUNT_TYPE_RESET = 'LOGIN_AUTH_ACCOUNT_TYPE_RESET'; +export const LOGIN_EMAIL_SEND = 'LOGIN_EMAIL_SEND'; export const LOGIN_FORM_UPDATE = 'LOGIN_FORM_UPDATE'; export const LOGIN_REQUEST = 'LOGIN_REQUEST'; export const LOGIN_REQUEST_FAILURE = 'LOGIN_REQUEST_FAILURE'; diff --git a/client/state/auth/actions.js b/client/state/auth/actions.js new file mode 100644 index 0000000000000..788da8ae4ac7c --- /dev/null +++ b/client/state/auth/actions.js @@ -0,0 +1,47 @@ +/** + * Internal dependencies + */ +import { LOGIN_EMAIL_SEND } from 'state/action-types'; +import { getLanguage, getLocaleSlug } from 'lib/i18n-utils'; +import 'state/data-layer/wpcom/auth/send-login-email'; + +/** + * Sends an email with a link that allows a user to login WordPress.com or the native apps + * + * @param {string} email - email to send to + * @param {object} options object: + * {string} redirectTo - url to redirect to after login + * {boolean} loginFormFlow - if true, dispatches actions associated with passwordless login + * {boolean} requestLoginEmailFormFlow - if true, dispatches actions associated with email me login + * {boolean} isMobileAppLogin - if true, will send an email that allows login to the native apps + * {boolean} showGlobalNotices - if true, displays global notices to user about the email + * + * @returns {object} action object + */ +export const sendEmailLogin = ( + email, + { + redirectTo, + showGlobalNotices = false, + loginFormFlow = false, + requestLoginEmailFormFlow = false, + isMobileAppLogin = false, + } +) => { + //Kind of weird usage, but this is a straight port from undocumented.js for now. + //I can move this to the caller, if there's equivalent info in the state tree + const locale = getLocaleSlug(); + const lang_id = getLanguage( locale ).value; + + return { + type: LOGIN_EMAIL_SEND, + email, + locale, + lang_id, + redirect_to: redirectTo, + isMobileAppLogin, + showGlobalNotices, + loginFormFlow, + requestLoginEmailFormFlow, + }; +}; diff --git a/client/state/data-layer/wpcom/auth/send-login-email/index.js b/client/state/data-layer/wpcom/auth/send-login-email/index.js new file mode 100644 index 0000000000000..9e33d5bfd38bb --- /dev/null +++ b/client/state/data-layer/wpcom/auth/send-login-email/index.js @@ -0,0 +1,141 @@ +/** + * External dependencies + */ +import { translate } from 'i18n-calypso'; + +/** + * Internal dependencies + */ +import { http } from 'state/data-layer/wpcom-http/actions'; +import { + LOGIN_EMAIL_SEND, + MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_FETCH, + MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_SUCCESS, + MAGIC_LOGIN_SHOW_CHECK_YOUR_EMAIL_PAGE, + MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_ERROR, +} from 'state/action-types'; +import { dispatchRequest } from 'state/data-layer/wpcom-http/utils'; +import { registerHandlers } from 'state/data-layer/handler-registry'; +import { infoNotice, errorNotice, successNotice, removeNotice } from 'state/notices/actions'; +import { recordTracksEventWithClientId } from 'state/analytics/actions'; +import config from 'config'; + +export const sendLoginEmail = action => { + const { + email, + lang_id, + locale, + redirect_to, + showGlobalNotices, + loginFormFlow, + requestLoginEmailFormFlow, + isMobileAppLogin, + } = action; + const noticeAction = showGlobalNotices + ? infoNotice( translate( 'Sending email' ), { duration: 4000 } ) + : null; + return [ + ...( showGlobalNotices ? [ noticeAction ] : [] ), + ...( loginFormFlow || requestLoginEmailFormFlow + ? [ { type: MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_FETCH } ] + : [] ), + ...( requestLoginEmailFormFlow + ? [ recordTracksEventWithClientId( 'calypso_login_email_link_submit' ) ] + : [] ), + ...( loginFormFlow + ? [ recordTracksEventWithClientId( 'calypso_login_block_login_form_send_magic_link' ) ] + : [] ), + http( + { + path: `/auth/send-login-email`, + method: 'POST', + apiVersion: '1.2', + body: { + client_id: config( 'wpcom_signup_id' ), + client_secret: config( 'wpcom_signup_key' ), + ...( isMobileAppLogin && { infer: true } ), + ...( isMobileAppLogin && { scheme: 'wordpress' } ), + locale, + lang_id: lang_id, + email: email, + ...( redirect_to && { redirect_to } ), + }, + }, + { ...action, infoNoticeId: noticeAction ? noticeAction.notice.noticeId : null } + ), + ]; +}; + +export const onSuccess = ( { + email, + showGlobalNotices, + infoNoticeId = null, + loginFormFlow, + requestLoginEmailFormFlow, +} ) => [ + ...( loginFormFlow || requestLoginEmailFormFlow + ? [ + { type: MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_SUCCESS }, + { type: MAGIC_LOGIN_SHOW_CHECK_YOUR_EMAIL_PAGE, email }, + ] + : [] ), + ...( requestLoginEmailFormFlow + ? [ recordTracksEventWithClientId( 'calypso_login_email_link_success' ) ] + : [] ), + ...( loginFormFlow + ? [ recordTracksEventWithClientId( 'calypso_login_block_login_form_send_magic_link_success' ) ] + : [] ), + // Default Global Notice Handling + ...( showGlobalNotices + ? [ + removeNotice( infoNoticeId ), + successNotice( translate( 'Email Sent. Check your mail app!' ), { + duration: 4000, + } ), + ] + : [] ), +]; + +export const onError = ( + { showGlobalNotices, infoNoticeId = null, loginFormFlow, requestLoginEmailFormFlow }, + error +) => [ + ...( loginFormFlow || requestLoginEmailFormFlow + ? [ { type: MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_ERROR, error: error.message } ] + : [] ), + ...( requestLoginEmailFormFlow + ? [ + recordTracksEventWithClientId( 'calypso_login_email_link_failure', { + error_code: error.error, + error_message: error.message, + } ), + ] + : [] ), + ...( loginFormFlow + ? [ + recordTracksEventWithClientId( 'calypso_login_block_login_form_send_magic_link_failure', { + error_code: error.error, + error_message: error.message, + } ), + ] + : [] ), + // Default Global Notice Handling + ...( showGlobalNotices + ? [ + removeNotice( infoNoticeId ), + errorNotice( translate( 'Sorry, we couldn’t send the email.' ), { + duration: 4000, + } ), + ] + : [] ), +]; + +registerHandlers( 'state/data-layer/wpcom/auth/send-login-email/index.js', { + [ LOGIN_EMAIL_SEND ]: [ + dispatchRequest( { + fetch: sendLoginEmail, + onSuccess, + onError, + } ), + ], +} ); diff --git a/client/state/login/magic-login/actions.js b/client/state/login/magic-login/actions.js index 3718f1ae59383..c5e6e69cabdb9 100644 --- a/client/state/login/magic-login/actions.js +++ b/client/state/login/magic-login/actions.js @@ -7,7 +7,6 @@ import { stringify } from 'qs'; * Internal dependencies */ import config from 'config'; -import wpcom from 'lib/wp'; import { AUTHENTICATE_URL } from './constants'; import { HTTPError } from '../utils'; import { @@ -17,9 +16,6 @@ import { MAGIC_LOGIN_REQUEST_AUTH_ERROR, MAGIC_LOGIN_REQUEST_AUTH_FETCH, MAGIC_LOGIN_REQUEST_AUTH_SUCCESS, - MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_ERROR, - MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_FETCH, - MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_SUCCESS, MAGIC_LOGIN_RESET_REQUEST_FORM, MAGIC_LOGIN_SHOW_LINK_EXPIRED, MAGIC_LOGIN_SHOW_CHECK_YOUR_EMAIL_PAGE, @@ -55,40 +51,6 @@ export const hideMagicLoginRequestNotice = () => { }; }; -/** - * Sends an email with a magic link to the specified email address. - * - * @param {string} email Email address of the user - * @param {string} redirectTo Url to redirect the user to upon successful login - * @returns {Function} A thunk that can be dispatched - */ -export const fetchMagicLoginRequestEmail = ( email, redirectTo ) => dispatch => { - dispatch( { type: MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_FETCH } ); - - return wpcom - .undocumented() - .requestMagicLoginEmail( { - email, - redirect_to: redirectTo, - } ) - .then( () => { - dispatch( { type: MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_SUCCESS } ); - - dispatch( { - type: MAGIC_LOGIN_SHOW_CHECK_YOUR_EMAIL_PAGE, - email, - } ); - } ) - .catch( error => { - dispatch( { - type: MAGIC_LOGIN_REQUEST_LOGIN_EMAIL_ERROR, - error: error.message, - } ); - - return Promise.reject( error ); - } ); -}; - async function postMagicLoginRequest( url, bodyObj ) { const response = await fetch( url, { method: 'POST',