diff --git a/projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-status-badges b/projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-status-badges
new file mode 100644
index 000000000000..0f223c62a91b
--- /dev/null
+++ b/projects/packages/premium-analytics/changelog/add-premium-analytics-earnings-status-badges
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Ads: Show payment status as a badge in the Earnings History widget, and shorten the pending statuses to one word with the reason beside them. Negative amounts in the widget are no longer red; only the badge carries colour.
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/earnings-history-list.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/earnings-history-list.test.tsx
index f6329411d536..24704f05c5b5 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/earnings-history-list.test.tsx
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/earnings-history-list.test.tsx
@@ -49,4 +49,13 @@ describe( 'EarningsHistoryList', () => {
expect( hiddenFlags() ).toEqual( [ false, true, true ] );
} );
+
+ it( 'renders each status as a badge, with a pending reason in an info button', () => {
+ mockSizes( 200, 36 );
+ render( );
+
+ expect( screen.getAllByText( 'Paid' ) ).toHaveLength( 3 );
+ expect( screen.getByText( 'Pending' ) ).toBeInTheDocument();
+ expect( screen.getByRole( 'button', { name: 'Missing tax info' } ) ).toBeInTheDocument();
+ } );
} );
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/fields.test.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/fields.test.tsx
index c02f07bb999a..220e93a6d8d8 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/fields.test.tsx
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/__tests__/fields.test.tsx
@@ -1,5 +1,6 @@
import { filterSortAndPaginate, type View } from '@jetpack-premium-analytics/externals';
import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
import {
EarningsStatusBadge,
flattenEarningsBreakdown,
@@ -12,8 +13,8 @@ describe( 'getEarningsStatus', () => {
expect( getEarningsStatus( 0 ).label ).toBe( 'Unpaid' );
expect( getEarningsStatus( 1 ).label ).toBe( 'Paid' );
expect( getEarningsStatus( 2 ).label ).toBe( 'a8c-only' );
- expect( getEarningsStatus( 3 ).label ).toBe( 'Pending (Missing Tax Info)' );
- expect( getEarningsStatus( 4 ).label ).toBe( 'Pending (Invalid PayPal)' );
+ expect( getEarningsStatus( 3 ).label ).toBe( 'Pending' );
+ expect( getEarningsStatus( 4 ).label ).toBe( 'Pending' );
} );
it( 'falls back to "?" for unknown or absent statuses', () => {
@@ -47,6 +48,18 @@ describe( 'EarningsStatusBadge', () => {
render( );
expect( screen.getByText( 'a8c-only' ) ).not.toHaveAttribute( 'tabindex' );
} );
+
+ it( 'puts a pending reason in an info button beside a one-word badge', async () => {
+ render( );
+
+ expect( screen.getByText( 'Pending' ) ).not.toHaveAttribute( 'tabindex' );
+
+ await userEvent.click( screen.getByRole( 'button', { name: 'Missing tax info' } ) );
+
+ await expect(
+ screen.findByText( /You can provide tax information in the settings screen/ )
+ ).resolves.toBeInTheDocument();
+ } );
} );
describe( 'flattenEarningsBreakdown', () => {
@@ -120,17 +133,31 @@ describe( 'getWordAdsHistoryFields', () => {
expect( data.map( row => row.period ) ).toEqual( [ period ] );
} );
- it( 'offers every status but a8c-only in the Status filter', () => {
+ it( 'offers every status but a8c-only in the Status filter, pending once', () => {
const status = fields.find( field => field.id === 'status' );
expect( status?.elements?.map( element => element.value ) ).toEqual( [
'Unpaid',
'Paid',
- 'Pending (Missing Tax Info)',
- 'Pending (Invalid PayPal)',
+ 'Pending',
] );
} );
+ it( 'filters "Pending" to both pending codes', () => {
+ const pending = [
+ ...rows,
+ { id: '2026-03', period: '2026-03', amount: 1, pageviews: 1, status: 3 },
+ { id: '2026-04', period: '2026-04', amount: 1, pageviews: 1, status: 4 },
+ ];
+ const { data } = filterSortAndPaginate(
+ pending,
+ { ...view, filters: [ { field: 'status', operator: 'is', value: 'Pending' } ] } as View,
+ fields
+ );
+
+ expect( data.map( row => row.period ) ).toEqual( [ '2026-03', '2026-04' ] );
+ } );
+
it.each( [
[ 'asc', [ '2025-12', '2026-09', '2012-03' ] ],
[ 'desc', [ '2026-09', '2025-12', '2012-03' ] ],
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.module.scss
index 3ac25172b4da..f67430345443 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.module.scss
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.module.scss
@@ -53,8 +53,3 @@
color: var(--wpds-color-foreground-content-neutral-weak);
text-align: end;
}
-
-/* Negative amounts and unpaid periods. */
-.attention {
- color: var(--wpds-color-foreground-content-error-weak);
-}
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.tsx
index 27447c4a2b91..9701143ca53d 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.tsx
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-history-list.tsx
@@ -9,7 +9,7 @@ import { useMemo } from 'react';
*/
import { useElementSize } from '../../hooks/use-element-size';
import styles from './earnings-history-list.module.scss';
-import { EarningsStatusLabel, formatEarningsPeriod, type EarningsHistoryRow } from './fields';
+import { EarningsStatusBadge, formatEarningsPeriod, type EarningsHistoryRow } from './fields';
export type EarningsHistoryListProps = {
rows?: EarningsHistoryRow[];
@@ -56,11 +56,9 @@ export function EarningsHistoryList( { rows = [], className }: EarningsHistoryLi
hidden={ index >= visibleCount }
>
{ formatEarningsPeriod( row.period ) }
-
- { formatMetricValue( row.amount, 'currency' ) }
-
-
-
+ { formatMetricValue( row.amount, 'currency' ) }
+
+
) ) }
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-status-badge.module.scss b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-status-badge.module.scss
new file mode 100644
index 000000000000..f727443da129
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/earnings-status-badge.module.scss
@@ -0,0 +1,30 @@
+/* Inline-flex blockifies the badge, so it stands its full line height plus
+ * padding, as in the design, and every status comes out the same height. */
+.root {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--wpds-dimension-gap-xs);
+ white-space: nowrap;
+}
+
+.info {
+ display: inline-flex;
+ align-items: center;
+ border: none;
+ padding: 0;
+ background: none;
+ color: var(--wpds-color-foreground-content-neutral-weak);
+ cursor: pointer;
+}
+
+.popup {
+ max-inline-size: 20rem;
+}
+
+/* The reason leads the explanation; the popover title is heading-sized,
+ * so it stays hidden. */
+.reason {
+ display: block;
+ margin-block-end: var(--wpds-dimension-gap-xs);
+ font-weight: var(--wpds-typography-font-weight-emphasis);
+}
diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/fields.tsx b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/fields.tsx
index b7dad0d742a1..6cfe7aa1ce6d 100644
--- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/fields.tsx
+++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/components/wordads-earnings-history/fields.tsx
@@ -2,13 +2,15 @@
* External dependencies
*/
import { parseSiteDateTime } from '@jetpack-premium-analytics/datetime';
-import { Badge } from '@jetpack-premium-analytics/externals';
+import { Badge, Icon, Popover, VisuallyHidden } from '@jetpack-premium-analytics/externals';
import { formatDate, formatMetricValue } from '@jetpack-premium-analytics/formatters';
import { Tooltip } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
+import { info } from '@wordpress/icons';
/**
* Internal dependencies
*/
+import styles from './earnings-status-badge.module.scss';
import type { StatsWordAdsEarningsBreakdown } from '@jetpack-premium-analytics/data';
import type { Field } from '@jetpack-premium-analytics/externals';
import type { ComponentProps } from 'react';
@@ -24,21 +26,27 @@ export type EarningsHistoryRow = {
type EarningsStatusIntent = NonNullable< ComponentProps< typeof Badge >[ 'intent' ] >;
-type EarningsStatus = { label: string; tooltip?: string; intent: EarningsStatusIntent };
+type EarningsStatus = {
+ label: string;
+ tooltip?: string;
+ intent: EarningsStatusIntent;
+ /** Why a payment is pending; shown beside the badge, not in it, so the label stays short. */
+ detail?: string;
+};
/** Automattic-internal status: never shown to site owners, so kept out of the Status filter. */
const A8C_ONLY_STATUS = 2;
/**
- * WordAds payment statuses by code, ported verbatim from the Jetpack Stats WordAds
- * `getStatus` map (wp-calypso client/my-sites/stats/wordads/earnings.jsx).
+ * WordAds payment statuses by code, adapted from the Jetpack Stats WordAds
+ * `getStatus` map (wp-calypso client/my-sites/stats/wordads/earnings.jsx),
*
- * @return The label, optional tooltip and badge intent for each known code.
+ * @return The label, optional tooltip, badge intent and pending detail for each known code.
*/
function getEarningsStatuses(): Record< number, EarningsStatus > {
return {
- // Unpaid is red as in the design and the widget; the pending codes wait on
- // the site owner, so they get the warning tint instead.
+ // Unpaid is red as in the design; the pending codes wait on the site owner,
+ // so they get the warning tint, one word, and the reason beside the badge.
0: {
label: __( 'Unpaid', 'jetpack-premium-analytics-pkg' ),
tooltip: __(
@@ -57,7 +65,8 @@ function getEarningsStatuses(): Record< number, EarningsStatus > {
intent: 'draft',
},
3: {
- label: __( 'Pending (Missing Tax Info)', 'jetpack-premium-analytics-pkg' ),
+ label: __( 'Pending', 'jetpack-premium-analytics-pkg' ),
+ detail: __( 'Missing tax info', 'jetpack-premium-analytics-pkg' ),
tooltip: __(
'Payment is pending due to missing information. You can provide tax information in the settings screen.',
'jetpack-premium-analytics-pkg'
@@ -65,7 +74,8 @@ function getEarningsStatuses(): Record< number, EarningsStatus > {
intent: 'medium',
},
4: {
- label: __( 'Pending (Invalid PayPal)', 'jetpack-premium-analytics-pkg' ),
+ label: __( 'Pending', 'jetpack-premium-analytics-pkg' ),
+ detail: __( 'Invalid PayPal', 'jetpack-premium-analytics-pkg' ),
tooltip: __(
'Payment processing has failed due to invalid PayPal address. You can correct the PayPal address in the settings screen.',
'jetpack-premium-analytics-pkg'
@@ -129,25 +139,6 @@ export function formatEarningsPeriod( period: string ): string {
return parsed ? formatDate( parsed, 'monthYear' ) : period;
}
-/**
- * A payment status label, with its explanation in a tooltip when there is one.
- *
- * @param props - The component props.
- * @param props.status - The numeric status from the earnings payload, if any.
- * @return The rendered label.
- */
-export function EarningsStatusLabel( { status }: { status: number | undefined } ) {
- const { label, tooltip } = getEarningsStatus( status );
-
- return tooltip ? (
-
- { label }
-
- ) : (
- { label }
- );
-}
-
/**
* Numeric sort that keeps rows without a count last in either direction.
*
@@ -168,22 +159,49 @@ function compareOptionalCounts( a: unknown, b: unknown, direction: 'asc' | 'desc
}
/**
- * A payment status as a badge, with its explanation in a tooltip when there is
- * one. The report table's rendering; the widget list keeps the plain label.
+ * A payment status as a badge. A pending status puts its reason in an info icon
+ * beside the badge; any other status keeps its explanation on the badge itself.
*
* @param props - The component props.
* @param props.status - The numeric status from the earnings payload, if any.
* @return The rendered badge.
*/
export function EarningsStatusBadge( { status }: { status: number | undefined } ) {
- const { label, tooltip, intent } = getEarningsStatus( status );
+ const { label, tooltip, intent, detail } = getEarningsStatus( status );
+
+ if ( detail ) {
+ // Click-open like the widget header's info icon; non-modal, so Tab leaves and closes it.
+ return (
+
+
+
+
+
+
+
+ }>{ detail }
+
+ { detail }
+ { tooltip }
+
+
+
+ { label }
+
+ );
+ }
+
const badge = (
{ label }
);
- return tooltip ? { badge } : badge;
+ return (
+
+ { tooltip ? { badge } : badge }
+
+ );
}
/**
@@ -227,9 +245,13 @@ export function getWordAdsHistoryFields(): Field< EarningsHistoryRow >[] {
// A filter rather than search: a substring match for "Paid" also finds "Unpaid".
// Sorts and filters by the visible label rather than the numeric code.
getValue: ( { item } ) => getEarningsStatus( item.status ).label,
+ // Both pending codes share a label, so one "Pending" option covers them.
elements: Object.entries( getEarningsStatuses() )
.filter( ( [ code ] ) => Number( code ) !== A8C_ONLY_STATUS )
- .map( ( [ , { label } ] ) => ( { value: label, label } ) ),
+ .map( ( [ , { label } ] ) => ( { value: label, label } ) )
+ .filter(
+ ( option, index, all ) => all.findIndex( o => o.value === option.value ) === index
+ ),
filterBy: { operators: [ 'is' ] },
render: ( { item } ) => ,
},
diff --git a/projects/packages/premium-analytics/routes/reports/earnings/page.tsx b/projects/packages/premium-analytics/routes/reports/earnings/page.tsx
index 8857c2367a63..3a606a794804 100644
--- a/projects/packages/premium-analytics/routes/reports/earnings/page.tsx
+++ b/projects/packages/premium-analytics/routes/reports/earnings/page.tsx
@@ -19,7 +19,7 @@ import {
type EarningsHistoryRow,
} from '@jetpack-premium-analytics/widgets-toolkit';
import { useMemo } from '@wordpress/element';
-import { __ } from '@wordpress/i18n';
+import { __, sprintf } from '@wordpress/i18n';
/**
* Internal dependencies
*/
@@ -95,8 +95,19 @@ function EarningsReport(): JSX.Element {
: [] ),
{
label: __( 'Status', 'jetpack-premium-analytics-pkg' ),
- // The numeric code says nothing to a reader of the export.
- getValue: row => getEarningsStatus( row.status ).label,
+ // The numeric code says nothing to a reader of the export; a pending
+ // row keeps its reason, which the table shows in an icon.
+ getValue: row => {
+ const { label, detail } = getEarningsStatus( row.status );
+ return detail
+ ? sprintf(
+ /* translators: 1: payment status, e.g. "Pending"; 2: the reason, e.g. "Missing tax info". */
+ __( '%1$s (%2$s)', 'jetpack-premium-analytics-pkg' ),
+ label,
+ detail
+ )
+ : label;
+ },
},
],
[ showAdsServed ]
diff --git a/projects/packages/premium-analytics/routes/reports/report-csv-exports.test.tsx b/projects/packages/premium-analytics/routes/reports/report-csv-exports.test.tsx
index 6c677c4f3d22..435cc755fe67 100644
--- a/projects/packages/premium-analytics/routes/reports/report-csv-exports.test.tsx
+++ b/projects/packages/premium-analytics/routes/reports/report-csv-exports.test.tsx
@@ -372,6 +372,23 @@ describe( 'report CSV exports', () => {
);
} );
+ it( 'exports a pending status with its reason', () => {
+ const rows = [ { id: '2026-09', period: '2026-09', amount: 30.25, pageviews: 300, status: 3 } ];
+ useEarningsReportRecordsMock.mockReturnValue( {
+ ...reportStatus,
+ tab: 'wordads',
+ availableTabs: [ 'wordads' ],
+ rows,
+ } as ReturnType< typeof useEarningsReportRecords > );
+
+ expectCsvExport( EarningsReportPage, 'earnings-wordads', rows, [
+ '2026-09',
+ 30.25,
+ 300,
+ 'Pending (Missing tax info)',
+ ] );
+ } );
+
it( 'configures the Clicks export with parent rows in hierarchy order', () => {
const group = { id: 'social', clickedUrl: 'Social', isGroup: true, clicks: 10 };
const lowerRow = {
diff --git a/projects/plugins/jetpack/changelog/add-premium-analytics-earnings-status-badges b/projects/plugins/jetpack/changelog/add-premium-analytics-earnings-status-badges
new file mode 100644
index 000000000000..cbd15bb5056c
--- /dev/null
+++ b/projects/plugins/jetpack/changelog/add-premium-analytics-earnings-status-badges
@@ -0,0 +1,4 @@
+Significance: patch
+Type: enhancement
+
+Premium Analytics: Show payment status as a badge in the Earnings History widget, and shorten the pending statuses to one word with the reason beside them. Negative amounts in the widget are no longer red; only the badge carries colour.
diff --git a/projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-status-badges b/projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-status-badges
new file mode 100644
index 000000000000..0f223c62a91b
--- /dev/null
+++ b/projects/plugins/premium-analytics/changelog/add-premium-analytics-earnings-status-badges
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Ads: Show payment status as a badge in the Earnings History widget, and shorten the pending statuses to one word with the reason beside them. Negative amounts in the widget are no longer red; only the badge carries colour.
diff --git a/projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-status-badges b/projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-status-badges
new file mode 100644
index 000000000000..4dc3b26eb0f7
--- /dev/null
+++ b/projects/plugins/wpcomsh/changelog/add-premium-analytics-earnings-status-badges
@@ -0,0 +1,4 @@
+Significance: patch
+Type: changed
+
+Premium Analytics: Show payment status as a badge in the Earnings History widget, and shorten the pending statuses to one word with the reason beside them. Negative amounts in the widget are no longer red; only the badge carries colour.