Commit 6d682244ca9 for woocommerce
commit 6d682244ca94735b96db6325ee537c805e1e107e
Author: Michal Iwanow <4765119+mcliwanow@users.noreply.github.com>
Date: Thu Aug 13 10:36:48 2026 +0200
Ignore a stale quality_badge marketplace filter param when the badge UI cannot render (#67665)
* Ignore a stale quality_badge param when the badge UI cannot render
The search requests appended quality_badge=1 from the URL alone, while
the toggle that clears it hides whenever the API reports the badge
disabled or the IAM settings fetch fails. With the badge enabled
server-side and settings unavailable client-side, the listing stayed
silently filtered with no control to clear it. The param now counts only
under the toggle's own visibility conditions, via a shared predicate.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Reset pagination when badge filter activation changes, not the raw param
The page-reset effect tracked query.quality_badge while the request
effects now key off isQualityBadgeFilterActive(). If IAM settings arrive
after the user paginated, the filter could activate without resetting to
page one, letting Load More append filtered results onto unfiltered
pages. Keying the reset on the same predicate closes that gap.
Also covers the enabled-but-empty-label case in the predicate tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Herman <KokkieH@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/fix-iam-settings-quality-badge-fix b/plugins/woocommerce/changelog/fix-iam-settings-quality-badge-fix
new file mode 100644
index 00000000000..0642a149914
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-iam-settings-quality-badge-fix
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Ignore a stale quality_badge URL param in marketplace search requests when the badge is disabled or IAM settings are unavailable, so the listing cannot stay silently filtered without the toggle that clears it.
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/content/content.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/content/content.tsx
index 05428dacad1..e0c16da2ae6 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/content/content.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/content/content.tsx
@@ -22,6 +22,7 @@ import Discover from '../discover/discover';
import Products from '../products/products';
import MySubscriptions from '../my-subscriptions/my-subscriptions';
import { MarketplaceContext } from '../../contexts/marketplace-context';
+import { isQualityBadgeFilterActive } from '../quality-badge/quality-badge-filter';
import { fetchSearchResults, getProductType } from '../../utils/functions';
import { SubscriptionsContextProvider } from '../../contexts/subscriptions-context';
import { SearchResultsCountType } from '../../contexts/types';
@@ -54,6 +55,11 @@ export default function Content(): React.JSX.Element {
const { isLoading, setIsLoading, selectedTab, setSearchResultsCount } =
marketplaceContextValue;
const query = useQuery();
+ // The param counts only while the toggle that clears it can render.
+ const qualityBadgeFilterActive = isQualityBadgeFilterActive(
+ query,
+ marketplaceContextValue.iamSettings
+ );
const searchCompleteAnnouncement = ( count: number ): void => {
speak(
@@ -92,7 +98,7 @@ export default function Content(): React.JSX.Element {
params.append( 'term', query.term );
}
- if ( query.quality_badge === '1' && query.tab === 'extensions' ) {
+ if ( qualityBadgeFilterActive && query.tab === 'extensions' ) {
params.append( 'quality_badge', '1' );
}
@@ -150,7 +156,7 @@ export default function Content(): React.JSX.Element {
query.category,
query.term,
query.tab,
- query.quality_badge,
+ qualityBadgeFilterActive,
setIsLoadingMore,
] );
@@ -205,7 +211,7 @@ export default function Content(): React.JSX.Element {
params.append( 'term', query.term );
}
- if ( query.quality_badge === '1' && query.tab === 'extensions' ) {
+ if ( qualityBadgeFilterActive && query.tab === 'extensions' ) {
params.append( 'quality_badge', '1' );
}
@@ -246,7 +252,7 @@ export default function Content(): React.JSX.Element {
// The badge filter only applies to the extensions results.
if (
category === 'extensions' &&
- query.quality_badge === '1'
+ qualityBadgeFilterActive
) {
params.append( 'quality_badge', '1' );
}
@@ -342,7 +348,7 @@ export default function Content(): React.JSX.Element {
query.tab,
query.term,
query.category,
- query.quality_badge,
+ qualityBadgeFilterActive,
setIsLoading,
setSearchResultsCount,
currentPage,
@@ -389,7 +395,12 @@ export default function Content(): React.JSX.Element {
useEffect( () => {
setCurrentPage( 1 );
setFirstNewProductId( 0 );
- }, [ selectedTab, query?.category, query?.term, query?.quality_badge ] );
+ }, [
+ selectedTab,
+ query?.category,
+ query?.term,
+ qualityBadgeFilterActive,
+ ] );
// Maintain product focus for accessibility
useEffect( () => {
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge-filter.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge-filter.tsx
index 759060eddc2..775fa13c6ad 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge-filter.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/quality-badge-filter.tsx
@@ -19,6 +19,7 @@ import { getNewPath, navigateTo, useQuery } from '@woocommerce/navigation';
import './quality-badge.scss';
import { QualityBadgeIcon, QualityBadgePopover } from './quality-badge';
import { MarketplaceContext } from '../../contexts/marketplace-context';
+import { MarketplaceContextType } from '../../contexts/types';
/**
* Info button next to the filter label; opens the shared badge explanation
@@ -68,6 +69,24 @@ function QualityBadgeInfo( props: {
);
}
+/**
+ * Whether the badge filter applies to outgoing search requests. Kept in sync
+ * with the toggle's own visibility conditions: the `quality_badge` URL param
+ * counts only while the control that clears it can render, so a stale or
+ * bookmarked param cannot silently filter the listing when the badge is
+ * disabled or the IAM settings failed to load.
+ */
+export function isQualityBadgeFilterActive(
+ query: { quality_badge?: string },
+ iamSettings: MarketplaceContextType[ 'iamSettings' ] | undefined
+): boolean {
+ const badge = iamSettings?.quality_badge;
+
+ return (
+ query.quality_badge === '1' && Boolean( badge?.enabled && badge.label )
+ );
+}
+
/**
* "Show only <badge>" toggle for product listings. Renders nothing unless the
* WooCommerce.com API reports the quality badge as enabled. The filter state
diff --git a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/test/quality-badge.test.tsx b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/test/quality-badge.test.tsx
index 0eb27ab4940..079860242d3 100644
--- a/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/test/quality-badge.test.tsx
+++ b/plugins/woocommerce/client/admin/client/marketplace/components/quality-badge/test/quality-badge.test.tsx
@@ -24,7 +24,9 @@ jest.mock( '@wordpress/a11y', () => ( {
import { speak } from '@wordpress/a11y';
import { navigateTo, useQuery } from '@woocommerce/navigation';
import QualityBadge from '../quality-badge';
-import QualityBadgeFilter from '../quality-badge-filter';
+import QualityBadgeFilter, {
+ isQualityBadgeFilterActive,
+} from '../quality-badge-filter';
import { MarketplaceContext } from '../../../contexts/marketplace-context';
import { MarketplaceContextType } from '../../../contexts/types';
import { Product, ProductType } from '../../product-list/types';
@@ -271,6 +273,47 @@ describe( 'QualityBadge', () => {
} );
} );
+describe( 'isQualityBadgeFilterActive', () => {
+ it( 'is active only when the param is set and the API has the badge enabled', () => {
+ expect(
+ isQualityBadgeFilterActive(
+ { quality_badge: '1' },
+ contextWithBadge.iamSettings
+ )
+ ).toBe( true );
+ } );
+
+ it( 'ignores a stale param when the badge is disabled', () => {
+ expect(
+ isQualityBadgeFilterActive(
+ { quality_badge: '1' },
+ contextWithBadgeDisabled.iamSettings
+ )
+ ).toBe( false );
+ } );
+
+ it( 'ignores a stale param when IAM settings are empty (failed fetch)', () => {
+ expect( isQualityBadgeFilterActive( { quality_badge: '1' }, {} ) ).toBe(
+ false
+ );
+ } );
+
+ it( 'ignores the param when the badge is enabled but has no label', () => {
+ expect(
+ isQualityBadgeFilterActive(
+ { quality_badge: '1' },
+ { quality_badge: { enabled: true, label: '', tooltip: '' } }
+ )
+ ).toBe( false );
+ } );
+
+ it( 'is inactive without the param', () => {
+ expect(
+ isQualityBadgeFilterActive( {}, contextWithBadge.iamSettings )
+ ).toBe( false );
+ } );
+} );
+
describe( 'QualityBadgeFilter', () => {
it( 'renders the toggle with the label from the API', () => {
renderWithContext( <QualityBadgeFilter />, contextWithBadge );