Date: Thu, 9 Jul 2026 10:56:13 -0300
Subject: [PATCH 3/6] Licensing: restore error-notice spacing and normalize its
text size
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The @wordpress/ui Notice sat flush against the input and Activate button, and
its rich errorInfo (raw from useGetErrorContent) inherited the parent's 16px
p rule while the Notice message rendered at Text body-md (13px) — leaving the
secondary help text larger than the primary message. Re-add a small stylesheet
that margins the notice and matches the errorInfo text to the notice size.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../components/activation-screen-error/index.tsx | 2 ++
.../components/activation-screen-error/style.scss | 13 +++++++++++++
2 files changed, 15 insertions(+)
create mode 100644 projects/js-packages/licensing/components/activation-screen-error/style.scss
diff --git a/projects/js-packages/licensing/components/activation-screen-error/index.tsx b/projects/js-packages/licensing/components/activation-screen-error/index.tsx
index 615f25307bf5..6771eb1919ee 100644
--- a/projects/js-packages/licensing/components/activation-screen-error/index.tsx
+++ b/projects/js-packages/licensing/components/activation-screen-error/index.tsx
@@ -5,6 +5,8 @@ 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 ];
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 000000000000..c8ade276b0e1
--- /dev/null
+++ b/projects/js-packages/licensing/components/activation-screen-error/style.scss
@@ -0,0 +1,13 @@
+.activation-screen-error {
+ margin: 16px 0;
+
+ // useGetErrorContent renders raw /
/; the parent controls set
+ // `p { font-size: 16px }`, so match them to the notice text size and let the
+ // wrapping Stack own the vertical spacing between them.
+ p,
+ ol,
+ ul {
+ margin: 0;
+ font-size: var(--wpds-typography-font-size-md);
+ }
+}
From 466b8b9ad8cb78fea5ffd327fd793cfe5fce1067 Mon Sep 17 00:00:00 2001
From: Christian Gastrell
Date: Thu, 9 Jul 2026 11:18:14 -0300
Subject: [PATCH 4/6] Licensing: use Notice.Title/Description for the error
notice
Use the Notice component's own grid layout (Title + Description in the same
column) instead of a hand-wrapped Stack, so the internal spacing/hierarchy come
from the design system. Render the Description as a since errorInfo is
rich block content (
/
/links). Keep a small external margin and normalize
the errorInfo text to the notice body size.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../activation-screen-error/index.tsx | 12 ++++++-----
.../activation-screen-error/style.scss | 20 ++++++++++++-------
2 files changed, 20 insertions(+), 12 deletions(-)
diff --git a/projects/js-packages/licensing/components/activation-screen-error/index.tsx b/projects/js-packages/licensing/components/activation-screen-error/index.tsx
index 6771eb1919ee..d802bb5aecf9 100644
--- a/projects/js-packages/licensing/components/activation-screen-error/index.tsx
+++ b/projects/js-packages/licensing/components/activation-screen-error/index.tsx
@@ -1,5 +1,5 @@
import jetpackAnalytics from '@automattic/jetpack-analytics';
-import { Notice, Stack, Text } from '@wordpress/ui';
+import { Notice } from '@wordpress/ui';
import { useEffect } from 'react';
import { LICENSE_ERRORS } from './constants';
import { useGetErrorContent } from './use-get-error-content';
@@ -38,10 +38,12 @@ const ActivationScreenError: FC< Props > = ( { licenseError, errorType } ) => {
className="activation-screen-error"
intent={ isLicenseAlreadyAttached ? 'success' : 'error' }
>
-
- { errorMessage }
- { errorInfo }
-
+ { errorMessage }
+ { errorInfo && (
+ // errorInfo is rich block content (/
/links), so render the
+ // Description as a rather than its default inline
.
+ }>{ errorInfo }
+ ) }
);
};
diff --git a/projects/js-packages/licensing/components/activation-screen-error/style.scss b/projects/js-packages/licensing/components/activation-screen-error/style.scss
index c8ade276b0e1..b00357728d8d 100644
--- a/projects/js-packages/licensing/components/activation-screen-error/style.scss
+++ b/projects/js-packages/licensing/components/activation-screen-error/style.scss
@@ -1,13 +1,19 @@
.activation-screen-error {
+ // External spacing from the input above and the Activate button below; the
+ // Notice's own grid owns the internal Title/Description spacing.
margin: 16px 0;
- // useGetErrorContent renders raw /
/; the parent controls set
- // `p { font-size: 16px }`, so match them to the notice text size and let the
- // wrapping Stack own the vertical spacing between them.
- p,
- ol,
- ul {
- margin: 0;
+ // errorInfo renders raw /
/ (from useGetErrorContent); the parent
+ // controls set `p { font-size: 16px }`, so match them to the notice body text.
+ :where(p, ol, ul) {
font-size: var(--wpds-typography-font-size-md);
}
+
+ p {
+ margin: 0 0 var(--wpds-dimension-gap-sm);
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
}
From 2b289ab3c32c9d79eadcb1912fa7e272537b0a01 Mon Sep 17 00:00:00 2001
From: Christian Gastrell
Date: Thu, 9 Jul 2026 15:41:30 -0300
Subject: [PATCH 5/6] Licensing: rebuild the activation controls layout with
@wordpress/ui Text + Stack
Replace the hand-rolled heading/paragraph and flex containers with @wordpress/ui
primitives: the h1 and intro copy become Text (heading-2xl / body-lg), and the
column layout becomes nested Stacks (outer space-between to pin the Activate
button at the bottom; a logo+heading group; a main-content group). Removes the
per-element margins/paddings and the flex CSS in favour of Stack gap.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../activation-screen-controls/index.jsx | 85 +++++++++++--------
.../activation-screen-controls/style.scss | 32 -------
2 files changed, 48 insertions(+), 69 deletions(-)
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 7eb5b8d25390..3e93d178f27e 100644
--- a/projects/js-packages/licensing/components/activation-screen-controls/index.jsx
+++ b/projects/js-packages/licensing/components/activation-screen-controls/index.jsx
@@ -2,7 +2,7 @@ import jetpackAnalytics from '@automattic/jetpack-analytics';
import { JetpackLogo } from '@automattic/jetpack-components';
import { createInterpolateElement } from '@wordpress/element';
import { sprintf, __ } from '@wordpress/i18n';
-import { Button, InputControl, SelectControl } from '@wordpress/ui';
+import { Button, InputControl, SelectControl, Stack, Text } from '@wordpress/ui';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useMemo, useState } from 'react';
import ActivationScreenError from '../activation-screen-error';
@@ -168,42 +168,53 @@ const ActivationScreenControls = props => {
const hasAvailableLicenseKey = availableLicenses && availableLicenses.length;
return (
-
-
-
-
{ __( 'Add a license key', 'jetpack-licensing' ) }
-
- { createInterpolateElement(
- __(
- 'Purchased a plan?
Check your email for your license key and paste it below.',
- 'jetpack-licensing'
- ),
- {
- strong: ,
- br:
,
- }
+
+
+
+
+ } variant="heading-2xl">
+ { __( 'Add a license key', 'jetpack-licensing' ) }
+
+
+
+
+ { createInterpolateElement(
+ __(
+ 'Purchased a plan?
Check your email for your license key and paste it below.',
+ 'jetpack-licensing'
+ ),
+ {
+ strong: ,
+ br:
,
+ }
+ ) }
+
+ { fetchingAvailableLicenses || hasAvailableLicenseKey ? (
+
+ ) : (
+
) }
-
- { fetchingAvailableLicenses || hasAvailableLicenseKey ? (
-
- ) : (
-
- ) }
- { 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 ee1f851490c2..6706eccccd5f 100644
--- a/projects/js-packages/licensing/components/activation-screen-controls/style.scss
+++ b/projects/js-packages/licensing/components/activation-screen-controls/style.scss
@@ -3,47 +3,15 @@
.jp-license-activation-screen-controls {
background: var(--jp-white);
padding: 32px;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
-
- h1 {
- // typography-exception
- font-size: 44px;
- font-weight: 700;
-
- // This is inherited in the Jetpack plugin from the top-level
- // Jetpack page. Doesn't carry over to My Jetpack.
- line-height: 1.4;
-
- // Exception here due to being overridden by Text component
- // styles when used in My Jetpack.
- margin: 0.67em 0;
- }
-
- p {
- font-size: var(--font-body);
-
- // Exception here due to being overridden by Text component
- // styles when used in My Jetpack.
- margin: 1em 0;
- }
-
- label {
- font-weight: 600;
- font-size: var(--font-body);
- }
@media screen and (min-width: 780px) {
padding: 64px;
}
-
.jp-license-activation-screen-controls--license-field,
.jp-license-activation-screen-controls--license-field-with-error,
.jp-license-activation-screen-controls--license-field-with-success {
max-width: 500px;
- margin-top: 10px;
}
.jp-license-activation-screen-controls--button {
From 388229444a30be6b47abe87d3f99baf76e59d37b Mon Sep 17 00:00:00 2001
From: Christian Gastrell
Date: Thu, 9 Jul 2026 16:49:16 -0300
Subject: [PATCH 6/6] Licensing: SelectControl manual-entry, two-column width,
and stale-error fixes
- Manual-entry option uses a sentinel value so the SelectControl shows its label
(not the empty-value placeholder) while the manual text input is displayed.
- Two-column layout: give each column a determinate width (flex:1 + min-width:0)
so a long SelectControl value truncates (built-in ellipsis) instead of
reflowing the columns; drop the now-redundant field width:100%.
- Clear the activation error when the license changes (select pick or manual
input) so a stale invalid-key notice doesn't linger.
- ProductLink companion link: add line-height so it doesn't look cramped on
mobile.
- Error notice: drop its external margin (the controls Stack gap owns spacing).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.../activation-screen-controls/index.jsx | 15 +++++++++++----
.../components/activation-screen-error/style.scss | 4 ----
.../product-link/style.scss | 1 +
.../components/activation-screen/index.jsx | 9 ++++++++-
.../components/activation-screen/style.scss | 8 ++++++++
5 files changed, 28 insertions(+), 9 deletions(-)
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 3e93d178f27e..b67355562854 100644
--- a/projects/js-packages/licensing/components/activation-screen-controls/index.jsx
+++ b/projects/js-packages/licensing/components/activation-screen-controls/index.jsx
@@ -9,6 +9,11 @@ import ActivationScreenError from '../activation-screen-error';
import { LICENSE_ERRORS } from '../activation-screen-error/constants';
import './style.scss';
+// Sentinel value for the "add a license key manually" option so the SelectControl
+// shows that option (rather than the empty-value placeholder) while the manual
+// text input is displayed.
+const MANUAL_ENTRY = '__manual_entry__';
+
/**
* The Activation Screen Controls component.
*
@@ -73,7 +78,7 @@ const SelectableLicenseKeyInput = props => {
} ),
{
label: __( 'I want to add a license key manually', 'jetpack-licensing' ),
- value: '',
+ value: MANUAL_ENTRY,
},
];
}, [ availableLicenses, isFetching ] );
@@ -90,7 +95,9 @@ const SelectableLicenseKeyInput = props => {
item => {
const val = item?.value ?? '';
setSelectedOption( val );
- onChange( val );
+ // Manual entry has no key yet — clear the stored license so the button
+ // stays disabled until the user types one.
+ onChange( val === MANUAL_ENTRY ? '' : val );
},
[ onChange ]
);
@@ -106,7 +113,7 @@ const SelectableLicenseKeyInput = props => {
onValueChange={ onSelectionChange }
/>
- { ! isFetching && ! selectedOption && (
+ { ! isFetching && selectedOption === MANUAL_ENTRY && (
{
className="jp-license-activation-screen-controls"
direction="column"
justify="space-between"
- gap="2xl"
+ gap="xl"
>
diff --git a/projects/js-packages/licensing/components/activation-screen-error/style.scss b/projects/js-packages/licensing/components/activation-screen-error/style.scss
index b00357728d8d..9714a377a670 100644
--- a/projects/js-packages/licensing/components/activation-screen-error/style.scss
+++ b/projects/js-packages/licensing/components/activation-screen-error/style.scss
@@ -1,8 +1,4 @@
.activation-screen-error {
- // External spacing from the input above and the Activate button below; the
- // Notice's own grid owns the internal Title/Description spacing.
- margin: 16px 0;
-
// errorInfo renders raw /
/ (from useGetErrorContent); the parent
// controls set `p { font-size: 16px }`, so match them to the notice body text.
:where(p, ol, ul) {
diff --git a/projects/js-packages/licensing/components/activation-screen-success-info/product-link/style.scss b/projects/js-packages/licensing/components/activation-screen-success-info/product-link/style.scss
index b6c45e39d806..cae168d34bb3 100644
--- a/projects/js-packages/licensing/components/activation-screen-success-info/product-link/style.scss
+++ b/projects/js-packages/licensing/components/activation-screen-success-info/product-link/style.scss
@@ -6,6 +6,7 @@
.jp-license-activation-screen-success-info--external-link {
font-weight: var(--wpds-typography-font-weight-medium);
font-size: var(--wpds-typography-font-size-lg);
+ line-height: var(--wpds-typography-line-height-2xl);
white-space: nowrap;
}
}
diff --git a/projects/js-packages/licensing/components/activation-screen/index.jsx b/projects/js-packages/licensing/components/activation-screen/index.jsx
index 09d71fbe0d8a..7cd412066943 100644
--- a/projects/js-packages/licensing/components/activation-screen/index.jsx
+++ b/projects/js-packages/licensing/components/activation-screen/index.jsx
@@ -79,6 +79,13 @@ const ActivationScreen = props => {
}
}, [ availableLicenses ] );
+ const onLicenseChange = useCallback( newLicense => {
+ setLicense( newLicense );
+ // Changing the license (via the select or the manual input) invalidates
+ // any prior activation error, so clear the stale notice.
+ setLicenseError( null );
+ }, [] );
+
const activateLicense = useCallback( () => {
if ( isSaving ) {
return Promise.resolve();
@@ -134,7 +141,7 @@ const ActivationScreen = props => {
isActivating={ isSaving }
license={ license }
licenseError={ licenseError }
- onLicenseChange={ setLicense }
+ onLicenseChange={ onLicenseChange }
siteUrl={ siteRawUrl }
/>
diff --git a/projects/js-packages/licensing/components/activation-screen/style.scss b/projects/js-packages/licensing/components/activation-screen/style.scss
index 74776caee3d4..3362edde0237 100644
--- a/projects/js-packages/licensing/components/activation-screen/style.scss
+++ b/projects/js-packages/licensing/components/activation-screen/style.scss
@@ -9,6 +9,14 @@
@media screen and ( min-width: 780px ) {
flex-direction: row;
+
+ // Give each column a determinate width (and allow shrinking) so the
+ // controls don't reflow when the SelectControl's selected value length
+ // changes — its trigger ellipsis engages instead.
+ > * {
+ flex: 1;
+ min-width: 0;
+ }
}
}