diff --git a/projects/js-packages/licensing/changelog/update-license-activate-error-messages b/projects/js-packages/licensing/changelog/update-license-activate-error-messages new file mode 100644 index 0000000000000..fb61373a9d180 --- /dev/null +++ b/projects/js-packages/licensing/changelog/update-license-activate-error-messages @@ -0,0 +1,4 @@ +Significance: minor +Type: changed + +Add more helpful error messages and help steps on license key activation error. diff --git a/projects/js-packages/licensing/components/activation-screen-controls/index.jsx b/projects/js-packages/licensing/components/activation-screen-controls/index.jsx index 72231125acb58..51bbfa6af8d0a 100644 --- a/projects/js-packages/licensing/components/activation-screen-controls/index.jsx +++ b/projects/js-packages/licensing/components/activation-screen-controls/index.jsx @@ -3,10 +3,10 @@ import { JetpackLogo, Spinner } from '@automattic/jetpack-components'; import { Button, TextControl, SelectControl } from '@wordpress/components'; import { createInterpolateElement } from '@wordpress/element'; import { sprintf, __ } from '@wordpress/i18n'; -import { Icon, warning } from '@wordpress/icons'; import PropTypes from 'prop-types'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; - +import ActivationScreenError from '../activation-screen-error'; +import { LICENSE_ERRORS } from '../activation-screen-error/constants'; import './style.scss'; /** @@ -145,15 +145,26 @@ const ActivationScreenControls = props => { licenseError, onLicenseChange, } = props; - const hasLicenseError = licenseError !== null && licenseError !== undefined; useEffect( () => { jetpackAnalytics.tracks.recordEvent( 'jetpack_wpa_license_key_activation_view' ); }, [] ); - const className = hasLicenseError - ? 'jp-license-activation-screen-controls--license-field-with-error' - : 'jp-license-activation-screen-controls--license-field'; + const errorTypeMatch = licenseError?.match( /\[[a-z_]+\]/ ); + const errorType = errorTypeMatch && errorTypeMatch[ 0 ]; + + const { ACTIVE_ON_SAME_SITE } = LICENSE_ERRORS; + const isLicenseAlreadyAttached = ACTIVE_ON_SAME_SITE === errorType; + const className = useMemo( () => { + if ( ! licenseError ) { + return 'jp-license-activation-screen-controls--license-field'; + } + if ( isLicenseAlreadyAttached ) { + return 'jp-license-activation-screen-controls--license-field-with-success'; + } + + return 'jp-license-activation-screen-controls--license-field-with-error'; + }, [ licenseError, isLicenseAlreadyAttached ] ); const hasAvailableLicenseKey = availableLicenses && availableLicenses.length; @@ -189,11 +200,8 @@ const ActivationScreenControls = props => { value={ license } /> ) } - { hasLicenseError && ( -
- - { licenseError } -
+ { licenseError && ( + ) }
diff --git a/projects/js-packages/licensing/components/activation-screen-controls/style.scss b/projects/js-packages/licensing/components/activation-screen-controls/style.scss index b6b0a0a0c3c89..70c8974ea348e 100644 --- a/projects/js-packages/licensing/components/activation-screen-controls/style.scss +++ b/projects/js-packages/licensing/components/activation-screen-controls/style.scss @@ -37,7 +37,8 @@ .jp-license-activation-screen-controls--license-field, - .jp-license-activation-screen-controls--license-field-with-error { + .jp-license-activation-screen-controls--license-field-with-error, + .jp-license-activation-screen-controls--license-field-with-success { max-width: 500px; .components-input-control__label.components-input-control__label.components-input-control__label { font-weight: 600; @@ -67,21 +68,10 @@ } } - .jp-license-activation-screen-controls--license-field-error { - display: flex; - flex-direction: row; - align-items: flex-start; - color: var(--jp-red); - max-width: 500px; - - svg { - margin-right: 4px; - fill: var(--jp-red); - min-width: 24px; - } - - span { - font-size: var(--font-body); + .jp-license-activation-screen-controls--license-field-with-success { + input.components-text-control__input, + select.components-select-control__input { + border: 1px solid var(--jp-green); } } diff --git a/projects/js-packages/licensing/components/activation-screen-controls/test/component.jsx b/projects/js-packages/licensing/components/activation-screen-controls/test/component.jsx index ad4fc05bdcbe8..b89a6d7c7e20d 100644 --- a/projects/js-packages/licensing/components/activation-screen-controls/test/component.jsx +++ b/projects/js-packages/licensing/components/activation-screen-controls/test/component.jsx @@ -47,7 +47,7 @@ describe( 'ActivationScreenControls', () => { expect( node ).toBeInTheDocument(); expect( // eslint-disable-next-line testing-library/no-node-access - node.closest( '.jp-license-activation-screen-controls--license-field-error' ) + node.closest( '.activation-screen-error__message' ) ).toBeInTheDocument(); } ); } ); diff --git a/projects/js-packages/licensing/components/activation-screen-error/constants.ts b/projects/js-packages/licensing/components/activation-screen-error/constants.ts new file mode 100644 index 0000000000000..d971f2625d79a --- /dev/null +++ b/projects/js-packages/licensing/components/activation-screen-error/constants.ts @@ -0,0 +1,9 @@ +export const LICENSE_ERRORS = { + NOT_SAME_OWNER: '[not_same_owner]', + ACTIVE_ON_SAME_SITE: '[active_on_same_site]', + ATTACHED_LICENSE: '[attached_license]', + PRODUCT_INCOMPATIBILITY: '[product_incompatibility]', + REVOKED_LICENSE: '[revoked_license]', + INVALID_INPUT: '[invalid_input]', + MULTISITE_INCOMPATIBILITY: '[multisite_incompatibility]', +} as const; diff --git a/projects/js-packages/licensing/components/activation-screen-error/index.tsx b/projects/js-packages/licensing/components/activation-screen-error/index.tsx new file mode 100644 index 0000000000000..0eec02d2f214d --- /dev/null +++ b/projects/js-packages/licensing/components/activation-screen-error/index.tsx @@ -0,0 +1,52 @@ +import jetpackAnalytics from '@automattic/jetpack-analytics'; +import { Icon, warning, check } from '@wordpress/icons'; +import React, { useEffect } from 'react'; +import { LICENSE_ERRORS } from './constants'; +import { useGetErrorContent } from './use-get-error-content'; +import type { FC } from 'react'; + +import './style.scss'; + +type LicenseErrorKeysType = keyof typeof LICENSE_ERRORS; +type LicenseErrorValuesType = ( typeof LICENSE_ERRORS )[ LicenseErrorKeysType ]; + +interface Props { + licenseError: string; + errorType: LicenseErrorValuesType; +} + +const ActivationScreenError: FC< Props > = ( { licenseError, errorType } ) => { + useEffect( () => { + if ( licenseError ) { + jetpackAnalytics.tracks.recordEvent( 'jetpack_wpa_license_activation_error_view', { + error: licenseError, + error_type: errorType, + } ); + } + }, [ licenseError, errorType ] ); + + const { errorMessage, errorInfo } = useGetErrorContent( licenseError, errorType ); + + if ( ! licenseError ) { + return null; + } + + const { ACTIVE_ON_SAME_SITE } = LICENSE_ERRORS; + const isLicenseAlreadyAttached = ACTIVE_ON_SAME_SITE === errorType; + + const errorMessageClass = isLicenseAlreadyAttached + ? 'activation-screen-error__message--success' + : 'activation-screen-error__message--error'; + + return ( + <> +
+ + { errorMessage } +
+ { errorInfo &&
{ errorInfo }
} + + ); +}; + +export default ActivationScreenError; diff --git a/projects/js-packages/licensing/components/activation-screen-error/style.scss b/projects/js-packages/licensing/components/activation-screen-error/style.scss new file mode 100644 index 0000000000000..5e92e10f6ef6c --- /dev/null +++ b/projects/js-packages/licensing/components/activation-screen-error/style.scss @@ -0,0 +1,75 @@ +.activation-screen-error__message { + display: flex; + flex-direction: row; + align-items: flex-start; + max-width: 500px; + margin-top: calc(var(--spacing-base)*.5); + + svg { + margin-left: -3px; + } + + span { + font-size: 13px; + line-height: 20px; + font-weight: 500; + letter-spacing: -0.044em; + } +} + +.activation-screen-error__message--error { + color: var(--jp-red); + svg { + fill: var(--jp-red); + } +} + +.activation-screen-error__message--success { + color: var(--jp-green); + svg { + fill: var(--jp-green); + } +} + +.activation-screen-error__info { + background-color: var(--jp-gray-0); + color: var(--jp-gray-80); + font-size: var(--font-body-small); + line-height: calc(var(--font-title-small) - 2px); + border: 1px solid var(--jp-green-0); + border-radius: var(--jp-border-radius); + padding: var(--jp-modal-padding-small); + margin: 32px 0 8px; + + > p { + margin: 0 0 1em; + font-size: var(--font-body-small); + } + > p:last-child { + margin-bottom: 0; + } + + ol > li::marker { + font-weight: bold; + } + + a { + color: var(--jp-green-50); + } + a:hover, + a:active { + color: var(--jp-green-70); + } +} + +.jp-license-activation-screen-controls { + .activation-screen-error__info { + > p { + margin: 0 0 1em; + font-size: var(--font-body-small); + } + > p:last-child { + margin-bottom: 0; + } + } +} diff --git a/projects/js-packages/licensing/components/activation-screen-error/use-get-error-content.tsx b/projects/js-packages/licensing/components/activation-screen-error/use-get-error-content.tsx new file mode 100644 index 0000000000000..45e259667611b --- /dev/null +++ b/projects/js-packages/licensing/components/activation-screen-error/use-get-error-content.tsx @@ -0,0 +1,185 @@ +import { getRedirectUrl } from '@automattic/jetpack-components'; +import { ExternalLink } from '@wordpress/components'; +import { createInterpolateElement } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import React from 'react'; +import { LICENSE_ERRORS } from './constants'; + +type LicenseErrorKeysType = keyof typeof LICENSE_ERRORS; +type LicenseErrorValuesType = ( typeof LICENSE_ERRORS )[ LicenseErrorKeysType ]; + +export const useGetErrorContent = ( licenseError: string, errorType: LicenseErrorValuesType ) => { + if ( ! licenseError ) { + return { + errorMessage: null, + errorInfo: null, + }; + } + + const needHelpGetInTouchLink = createInterpolateElement( + __( 'Need help? Get in touch.', 'jetpack' ), + { + a: ( + + ), + } + ); + + switch ( errorType ) { + case LICENSE_ERRORS.NOT_SAME_OWNER: + return { + errorMessage: __( + 'The account that purchased the plan and the account managing this site are different.', + 'jetpack' + ), + errorInfo: ( + <> +

+ { createInterpolateElement( + __( 'Follow these steps to resolve it.', 'jetpack' ), + { + a: ( + + ), + } + ) } +

+
    +
  1. { __( 'Disconnect Jetpack from your site.', 'jetpack' ) }
  2. +
  3. + { __( 'Log in to the WordPress.com account that purchased the plan.', 'jetpack' ) } +
  4. +
  5. { __( 'Reconnect Jetpack.', 'jetpack' ) }
  6. +
+

{ needHelpGetInTouchLink }

+ + ), + }; + case LICENSE_ERRORS.ACTIVE_ON_SAME_SITE: + return { + errorMessage: __( 'This license is already active on your site.', 'jetpack' ), + errorInfo: null, + }; + case LICENSE_ERRORS.ATTACHED_LICENSE: + return { + errorMessage: __( 'This license is already active on another website', 'jetpack' ), + errorInfo: ( + + ), + }; + case LICENSE_ERRORS.PRODUCT_INCOMPATIBILITY: + return { + errorMessage: __( + 'Your site already has an active Jetpack plan of equal or higher value.', + 'jetpack' + ), + errorInfo: ( + <> +

+ { __( + 'It looks like your website already has a Jetpack plan that’s equal to or better than the one you’re trying to activate.', + 'jetpack' + ) } +

+ +

+ { __( + 'You can either use this license on a different site or cancel your current plan for a refund.', + 'jetpack' + ) } +

+

{ needHelpGetInTouchLink }

+ + ), + }; + case LICENSE_ERRORS.REVOKED_LICENSE: + return { + errorMessage: __( + 'The subscription is no longer active or has expired. Please purchase a new license.', + 'jetpack' + ), + errorInfo:

{ needHelpGetInTouchLink }

, + }; + case LICENSE_ERRORS.INVALID_INPUT: + return { + errorMessage: __( 'Unable to validate this license key.', 'jetpack' ), + errorInfo: ( + <> +

+ { __( + 'Please take a moment to check the license key from your purchase confirmation email—it might have a small typo.', + 'jetpack' + ) } +

+ +

{ needHelpGetInTouchLink }

+ + ), + }; + case LICENSE_ERRORS.MULTISITE_INCOMPATIBILITY: { + const planNameMatch = licenseError.match( /We.re sorry, (.*) is not compatible/ ); + const planName = planNameMatch && planNameMatch[ 1 ]; + return { + errorMessage: sprintf( + /* translators: %s is the Jetpack product name, i.e.- Jetpack Backup, Jetpack Boost, etc., which the product name should not be translated. */ + __( + 'We’re sorry, %s is not compatible with multisite WordPress installations at this time.', + 'jetpack' + ), + planName + ), + errorInfo: ( + <> +

+ { __( + 'This Jetpack plan doesn’t work with Multisite WordPress setups. Please use it on a single-site installation or consider canceling for a refund.', + 'jetpack' + ) } +

+

{ needHelpGetInTouchLink }

+ + ), + }; + } + default: + return { + errorMessage: licenseError, + errorInfo:

{ needHelpGetInTouchLink }

, + }; + } +};