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 && (
-
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: (
+
+ ),
+ }
+ ) }
+
+
+ - { __( 'Disconnect Jetpack from your site.', 'jetpack' ) }
+ -
+ { __( 'Log in to the WordPress.com account that purchased the plan.', 'jetpack' ) }
+
+ - { __( 'Reconnect Jetpack.', 'jetpack' ) }
+
+
{ 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: (
+
+ -
+ { createInterpolateElement(
+ __( 'If you would like to transfer it, please get in touch.', 'jetpack' ),
+ {
+ a: (
+
+ ),
+ }
+ ) }
+
+ -
+ { createInterpolateElement(
+ __( 'To use Jetpack on both sites, please buy another license.', 'jetpack' ),
+ {
+ a: (
+
+ ),
+ }
+ ) }
+
+
+ ),
+ };
+ 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 }
,
+ };
+ }
+};