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.