diff --git a/src/components/Tables/PaymentActivityTable/PaymentActivityTableRow.tsx b/src/components/Tables/PaymentActivityTable/PaymentActivityTableRow.tsx
new file mode 100644
index 000000000000..4d2748bc52eb
--- /dev/null
+++ b/src/components/Tables/PaymentActivityTable/PaymentActivityTableRow.tsx
@@ -0,0 +1,54 @@
+import AccountAvatar from '@components/Avatar/connected/AccountAvatar';
+import Table from '@components/Table';
+import TextWithTooltip from '@components/TextWithTooltip';
+
+import useThemeStyles from '@hooks/useThemeStyles';
+
+import React from 'react';
+import {View} from 'react-native';
+
+import type {PaymentActivityRowData} from '.';
+
+type PaymentActivityTableRowProps = {
+ /** The active user shown in the row */
+ item: PaymentActivityRowData;
+
+ /** The index of the row relative to all other rows */
+ rowIndex: number;
+};
+
+/** A read-only row showing an active user's avatar, name, and email. */
+export default function PaymentActivityTableRow({item, rowIndex}: PaymentActivityTableRowProps) {
+ const styles = useThemeStyles();
+
+ const accessibilityLabel = [item.name, item.email].filter(Boolean).join(', ');
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/Tables/PaymentActivityTable/index.tsx b/src/components/Tables/PaymentActivityTable/index.tsx
new file mode 100644
index 000000000000..ac52770a329e
--- /dev/null
+++ b/src/components/Tables/PaymentActivityTable/index.tsx
@@ -0,0 +1,95 @@
+import type {CompareItemsCallback, IsItemInSearchCallback, TableColumn, TableData} from '@components/Table';
+import Table, {composeTableListHeader} from '@components/Table';
+
+import useLocalize from '@hooks/useLocalize';
+
+import tokenizedSearch from '@libs/tokenizedSearch';
+
+import CONST from '@src/CONST';
+
+import type {ListRenderItemInfo} from '@shopify/flash-list';
+
+import React from 'react';
+
+import PaymentActivityTableRow from './PaymentActivityTableRow';
+
+type PaymentActivityTableColumnKey = 'member';
+
+type PaymentActivityRowData = TableData & {
+ accountID: number;
+
+ /** The active user's login (email/phone) */
+ login: string;
+
+ /** The active user's display name */
+ name: string;
+
+ /** The active user's formatted email/phone shown as the subtitle */
+ email: string;
+};
+
+type PaymentActivityTableProps = {
+ /** The active users billed for the month */
+ activeUsers: PaymentActivityRowData[];
+
+ /** Page-level content rendered above the table header inside the scrollable list */
+ headerComponent?: React.ReactElement;
+};
+
+function PaymentActivityTable({activeUsers, headerComponent}: PaymentActivityTableProps) {
+ const {translate, localeCompare} = useLocalize();
+
+ // The count always reflects every active user, not just the ones matching the search.
+ const activeUsersLabel = translate('subscription.paymentHistory.activeUsers', {count: activeUsers.length});
+
+ const columns: Array> = [
+ {
+ key: 'member',
+ label: activeUsersLabel,
+ sortable: false,
+ },
+ ];
+
+ const compareItems: CompareItemsCallback = (item1, item2) => localeCompare(item1.name.toLowerCase(), item2.name.toLowerCase());
+
+ const isItemInSearch: IsItemInSearchCallback = (item, searchValue) => {
+ const results = tokenizedSearch([item], searchValue, (option) => [option.name, option.email, option.login]);
+ return results.length > 0;
+ };
+
+ const renderItem = ({item, index}: ListRenderItemInfo) => (
+
+ );
+
+ // Short lists are easy to scan, so the search bar only shows once there are enough active users to search through.
+ const shouldShowSearchBar = activeUsers.length >= CONST.STANDARD_LIST_ITEM_LIMIT;
+ const tableHeaderComponent = composeTableListHeader(
+ headerComponent,
+ shouldShowSearchBar ? : undefined,
+ );
+
+ return (
+ item.keyForList}
+ >
+ {tableHeaderComponent}
+
+
+
+
+ );
+}
+
+export default PaymentActivityTable;
+
+export type {PaymentActivityRowData};
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 04fab8db3589..f9e117d24c79 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -11628,6 +11628,8 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `${amount} erstattet am ${date}`,
},
billedTo: 'Abgerechnet an',
+ activity: 'Aktivität',
+ findActiveUser: 'Aktiven Benutzer suchen',
viewReceipt: 'Beleg anzeigen',
viewInvoice: 'Rechnung anzeigen',
},
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 1effc2a698aa..2be7a6c0216e 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -11844,6 +11844,8 @@ ${reportName}`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `Επιστράφηκαν ${amount} στις ${date}`,
},
billedTo: 'Χρέωση σε',
+ activity: 'Δραστηριότητα',
+ findActiveUser: 'Εύρεση ενεργού χρήστη',
viewReceipt: 'Προβολή απόδειξης',
viewInvoice: 'Προβολή τιμολογίου',
},
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 77fec2e2bbab..bbbaccf97a5d 100644
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -11897,6 +11897,8 @@ const translations = {
refundedAmountOn: ({amount, date}: PaymentDetailsPartialRefundParams) => `Refunded ${amount} on ${date}`,
},
billedTo: 'Billed to',
+ activity: 'Activity',
+ findActiveUser: 'Find an active user',
viewReceipt: 'View receipt',
viewInvoice: 'View invoice',
},
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 67f6933ea2a0..79d1464c8ec7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -11808,6 +11808,8 @@ ${reportName}`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `Reembolsado ${amount} el ${date}`,
},
billedTo: 'Facturado a',
+ activity: 'Actividad',
+ findActiveUser: 'Buscar un usuario activo',
viewReceipt: 'Ver recibo',
viewInvoice: 'Ver factura',
},
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 0885acea6f58..c9558cfd65ce 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -11654,6 +11654,8 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `${amount} remboursé le ${date}`,
},
billedTo: 'Facturé à',
+ activity: 'Activité',
+ findActiveUser: 'Rechercher un utilisateur actif',
viewReceipt: 'Voir le reçu',
viewInvoice: 'Voir la facture',
},
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 45bffa78d3b0..6a83da557e7f 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -11574,6 +11574,8 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `Rimborsati ${amount} il ${date}`,
},
billedTo: 'Fatturato a',
+ activity: 'Attività',
+ findActiveUser: 'Trova un utente attivo',
viewReceipt: 'Visualizza ricevuta',
viewInvoice: 'Visualizza fattura',
},
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index ba614e4577ed..358da21657d1 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -11408,6 +11408,8 @@ ${reportName}`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `${date} に ${amount} を返金`,
},
billedTo: '請求先',
+ activity: 'アクティビティ',
+ findActiveUser: 'アクティブユーザーを検索',
viewReceipt: '領収書を表示',
viewInvoice: '請求書を表示',
},
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index ff7257d04879..291d1352e8a5 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -11539,6 +11539,8 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `${amount} terugbetaald op ${date}`,
},
billedTo: 'Gefactureerd aan',
+ activity: 'Activiteit',
+ findActiveUser: 'Zoek een actieve gebruiker',
viewReceipt: 'Bon bekijken',
viewInvoice: 'Factuur bekijken',
},
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index ddae7042d2a0..76629f690e54 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -11560,6 +11560,8 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `Zwrócono ${amount} ${date}`,
},
billedTo: 'Obciążono',
+ activity: 'Aktywność',
+ findActiveUser: 'Znajdź aktywnego użytkownika',
viewReceipt: 'Wyświetl paragon',
viewInvoice: 'Wyświetl fakturę',
},
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 9087c58c1d7e..ee3863a4a0f7 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -11532,6 +11532,8 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `${amount} reembolsado em ${date}`,
},
billedTo: 'Cobrado de',
+ activity: 'Atividade',
+ findActiveUser: 'Encontrar um usuário ativo',
viewReceipt: 'Ver recibo',
viewInvoice: 'Ver fatura',
},
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 77c3b22319b6..2fd413e6f9dd 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -11115,6 +11115,8 @@ ${reportName}`,
refundedAmountOn: ({amount, date}: {amount: string; date: string}) => `${date} 已退款 ${amount}`,
},
billedTo: '账单对象',
+ activity: '活动',
+ findActiveUser: '查找活跃用户',
viewReceipt: '查看收据',
viewInvoice: '查看发票',
},
diff --git a/src/libs/PaymentHistoryUtils.ts b/src/libs/PaymentHistoryUtils.ts
index bc1a67e9b1a8..595795ffdd85 100644
--- a/src/libs/PaymentHistoryUtils.ts
+++ b/src/libs/PaymentHistoryUtils.ts
@@ -203,6 +203,11 @@ function getActiveUserCount(purchase: Purchase): number | undefined {
return emails.size > 0 ? emails.size : undefined;
}
+/** Returns the account IDs of the users billed for a purchase, deduped since the same person can be billed under more than one workspace. */
+function getActiveUserAccountIDs(purchase: Purchase | undefined): number[] {
+ return [...new Set(purchase?.activeUsers?.filter((accountID) => !!accountID))];
+}
+
function compareNewestFirst(left: PaymentHistoryRow, right: PaymentHistoryRow): number {
if (left.created === right.created) {
return right.purchaseID - left.purchaseID;
@@ -370,6 +375,7 @@ function getReceiptFileName(source: string, purchaseID: number): string {
export {
formatDate,
+ getActiveUserAccountIDs,
getBadgeAppearance,
getBadgeText,
getPaymentDetailsBadge,
diff --git a/src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx b/src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx
index 8178de44ae0e..debdcb40234c 100644
--- a/src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx
+++ b/src/pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage.tsx
@@ -4,7 +4,8 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButtonAndTitle from '@components/Header/composed/HeaderWithBackButtonAndTitle';
import MenuItem from '@components/MenuItem';
import ScreenWrapper from '@components/ScreenWrapper';
-import ScrollView from '@components/ScrollView';
+import PaymentActivityTable from '@components/Tables/PaymentActivityTable';
+import type {PaymentActivityRowData} from '@components/Tables/PaymentActivityTable';
import Text from '@components/Text';
import UserPill from '@components/UserPill';
@@ -13,14 +14,23 @@ import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
-import {usePersonalDetail} from '@hooks/usePersonalDetails';
+import {usePersonalDetail, usePersonalDetailsByIDs} from '@hooks/usePersonalDetails';
import useThemeStyles from '@hooks/useThemeStyles';
import {convertAmountToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
-import {formatDate, getBadgeAppearance, getBadgeText, getPaymentDetailsBadge, getPaymentHistoryRow, getReceiptSource, isInvoiceBilling} from '@libs/PaymentHistoryUtils';
+import {
+ formatDate,
+ getActiveUserAccountIDs,
+ getBadgeAppearance,
+ getBadgeText,
+ getPaymentDetailsBadge,
+ getPaymentHistoryRow,
+ getReceiptSource,
+ isInvoiceBilling,
+} from '@libs/PaymentHistoryUtils';
import {temporaryGetDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
@@ -59,6 +69,8 @@ function PaymentDetailsPage({route}: PaymentDetailsPageProps) {
const billedToAccountID = purchase?.billedToAccountID;
const [billedToDetails] = usePersonalDetail(billedToAccountID);
const row = getPaymentHistoryRow(purchases, purchaseID);
+ const activeUserAccountIDs = getActiveUserAccountIDs(purchase);
+ const [activeUserPersonalDetails] = usePersonalDetailsByIDs(activeUserAccountIDs);
if (!isBetaOn || (purchases !== undefined && !row)) {
return ;
@@ -76,6 +88,90 @@ function PaymentDetailsPage({route}: PaymentDetailsPageProps) {
const isInvoice = isInvoiceBilling(purchases, purchaseID);
const billedToName = temporaryGetDisplayNameOrDefault({passedPersonalDetails: billedToDetails, translate, formatPhoneNumber});
+
+ const activeUsers: PaymentActivityRowData[] = activeUserAccountIDs.map((accountID) => {
+ const details = activeUserPersonalDetails?.[accountID];
+ return {
+ keyForList: String(accountID),
+ accountID,
+ login: details?.login ?? '',
+ name: temporaryGetDisplayNameOrDefault({passedPersonalDetails: details, translate, formatPhoneNumber}),
+ email: formatPhoneNumber(details?.login ?? ''),
+ };
+ });
+ const hasActiveUsers = activeUsers.length > 0;
+
+ const detailsContent = (
+ <>
+ {!!row && (
+
+
+
+ {dateLabel}
+
+
+ {amountLabel}
+
+
+
+
+ {row.showsTax && {translate('subscription.paymentHistory.inclTax')}}
+
+
+ )}
+ {!!billedToName && (
+
+ {translate('subscription.paymentHistory.billedTo')}
+
+
+ )}
+ {!!receiptSource && (
+ {
+ Navigation.navigate(ROUTES.BILLING_RECEIPT.getRoute(purchaseID));
+ }}
+ sentryLabel={CONST.SENTRY_LABEL.SETTINGS_SUBSCRIPTION.VIEW_BILLING_RECEIPT}
+ accessibilityLabel={translate(isInvoice ? 'subscription.paymentHistory.viewInvoice' : 'subscription.paymentHistory.viewReceipt')}
+ style={[styles.ph5, styles.pv4]}
+ >
+
+
+
+ {translate(isInvoice ? 'subscription.paymentHistory.viewInvoice' : 'subscription.paymentHistory.viewReceipt')}
+
+
+
+
+ )}
+ {hasActiveUsers && (
+
+ {translate('subscription.paymentHistory.activity')}
+
+ )}
+ >
+ );
+
return (
-
- {!!row && (
-
-
-
- {dateLabel}
-
-
- {amountLabel}
-
-
-
-
- {row.showsTax && {translate('subscription.paymentHistory.inclTax')}}
-
-
- )}
- {!!billedToName && (
-
- {translate('subscription.paymentHistory.billedTo')}
-
-
- )}
- {!!receiptSource && (
- {
- Navigation.navigate(ROUTES.BILLING_RECEIPT.getRoute(purchaseID));
- }}
- sentryLabel={CONST.SENTRY_LABEL.SETTINGS_SUBSCRIPTION.VIEW_BILLING_RECEIPT}
- accessibilityLabel={translate(isInvoice ? 'subscription.paymentHistory.viewInvoice' : 'subscription.paymentHistory.viewReceipt')}
- style={[styles.ph5, styles.pv4]}
- >
-
-
-
-
- {translate(isInvoice ? 'subscription.paymentHistory.viewInvoice' : 'subscription.paymentHistory.viewReceipt')}
-
-
-
-
-
- )}
-
+
+
+
);
diff --git a/tests/ui/PaymentDetailsPageTest.tsx b/tests/ui/PaymentDetailsPageTest.tsx
new file mode 100644
index 000000000000..8445dafc4c8c
--- /dev/null
+++ b/tests/ui/PaymentDetailsPageTest.tsx
@@ -0,0 +1,148 @@
+import {fireEvent, render, screen, waitFor} from '@testing-library/react-native';
+
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxListItemProvider from '@components/OnyxListItemProvider';
+import PersonalDetailsByLoginProvider from '@components/PersonalDetailsByLoginProvider';
+
+import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator';
+
+import type {SettingsNavigatorParamList} from '@navigation/types';
+
+import PaymentDetailsPage from '@pages/settings/Subscription/PaymentHistoryPage/PaymentDetailsPage';
+
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import SCREENS from '@src/SCREENS';
+import type {Purchase} from '@src/types/onyx/PurchaseList';
+
+import {PortalProvider} from '@gorhom/portal';
+import {NavigationContainer} from '@react-navigation/native';
+import React from 'react';
+import Onyx from 'react-native-onyx';
+
+import * as TestHelper from '../utils/TestHelper';
+import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
+
+TestHelper.setupGlobalFetchMock();
+
+// The page fetches the purchase details on mount; the tests seed that data in Onyx instead.
+jest.mock('@userActions/Subscription', () => ({
+ __esModule: true,
+ openPaymentDetailsPage: jest.fn(),
+}));
+
+const PURCHASE_ID = 1001;
+const BOB = {accountID: 2, login: 'bob@example.com', displayName: 'Bob Clyde'};
+const TODD = {accountID: 3, login: 'todd@example.com', displayName: 'Todd Clyde'};
+
+// Enough extra members to reach the search bar threshold together with Bob and Todd
+const OTHER_MEMBERS = Array.from({length: CONST.STANDARD_LIST_ITEM_LIMIT - 2}, (_, index) => ({
+ accountID: 100 + index,
+ login: `member${index}@example.com`,
+ displayName: `Member ${index}`,
+}));
+const SEARCH_PLACEHOLDER = 'Find an active user';
+
+const Stack = createPlatformStackNavigator();
+
+function renderPage() {
+ return render(
+
+
+
+
+
+
+
+
+ ,
+ );
+}
+
+async function setPurchase(activeUsers: number[], billingType?: string) {
+ const purchase: Purchase = {
+ purchaseID: PURCHASE_ID,
+ created: '2026-02-01 00:00:00',
+ amount: 1000,
+ currency: CONST.CURRENCY.USD,
+ message: {billingType},
+ activeUsers,
+ isLoaded: true,
+ };
+ await Onyx.merge(ONYXKEYS.PURCHASES, {[PURCHASE_ID]: purchase});
+ await waitForBatchedUpdatesWithAct();
+}
+
+describe('PaymentDetailsPage Activity list', () => {
+ beforeEach(async () => {
+ Onyx.init({keys: ONYXKEYS});
+ await Onyx.clear();
+ await Onyx.merge(ONYXKEYS.BETAS, [CONST.BETAS.PAYMENT_HISTORY]);
+ await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, Object.fromEntries([BOB, TODD, ...OTHER_MEMBERS].map((member) => [member.accountID, member])));
+ await waitForBatchedUpdatesWithAct();
+ });
+
+ afterEach(async () => {
+ await Onyx.clear();
+ });
+
+ it('lists each billed member once, with the deduped count under the Activity header', async () => {
+ // Given a month where Todd was billed under two workspaces
+ await setPurchase([TODD.accountID, BOB.accountID, TODD.accountID]);
+
+ // When the details page renders
+ renderPage();
+ await waitForBatchedUpdatesWithAct();
+
+ // Then the count matches the deduped members and each member is listed with their email
+ expect(await screen.findByText('Activity')).toBeOnTheScreen();
+ expect(screen.getByText('2 active users')).toBeOnTheScreen();
+ expect(screen.getByText(BOB.displayName)).toBeOnTheScreen();
+ expect(screen.getByText(TODD.displayName)).toBeOnTheScreen();
+ expect(screen.getByText(BOB.login)).toBeOnTheScreen();
+
+ // And the search bar is hidden because two members are quick to scan
+ expect(screen.queryByPlaceholderText(SEARCH_PLACEHOLDER)).toBeNull();
+ });
+
+ it('filters the list as the user searches once there are enough members to search', async () => {
+ // Given a month with enough billed members to show the search bar
+ await setPurchase([TODD.accountID, BOB.accountID, ...OTHER_MEMBERS.map((member) => member.accountID)]);
+ renderPage();
+ await waitForBatchedUpdatesWithAct();
+ await screen.findByText(BOB.displayName);
+
+ // When the user searches for one of them
+ fireEvent.changeText(screen.getByPlaceholderText(SEARCH_PLACEHOLDER), 'bob');
+
+ // Then only the matching member stays in the list, and the count still shows every active user
+ await waitFor(() => expect(screen.queryByText(TODD.displayName)).toBeNull());
+ expect(screen.getByText(BOB.displayName)).toBeOnTheScreen();
+ expect(screen.getByText(`${CONST.STANDARD_LIST_ITEM_LIMIT} active users`)).toBeOnTheScreen();
+
+ // And a search that matches nobody shows the no-results message
+ fireEvent.changeText(screen.getByPlaceholderText(SEARCH_PLACEHOLDER), 'zzzz');
+ expect(await screen.findByText('No results found')).toBeOnTheScreen();
+ expect(screen.queryByText(BOB.displayName)).toBeNull();
+ });
+
+ it('hides the Activity section for a month with no billed members', async () => {
+ // Given a failed month nobody was billed for
+ await setPurchase([], CONST.BILLING.TYPE_FAILED);
+
+ // When the details page renders
+ renderPage();
+ await waitForBatchedUpdatesWithAct();
+
+ // Then the details still show, but the Activity label, the search field, and the count are all hidden
+ expect(await screen.findByText('$10.00')).toBeOnTheScreen();
+ expect(screen.queryByText('Activity')).toBeNull();
+ expect(screen.queryByPlaceholderText(SEARCH_PLACEHOLDER)).toBeNull();
+ expect(screen.queryByText(/active user/)).toBeNull();
+ });
+});
diff --git a/tests/unit/PaymentHistoryUtilsTest.ts b/tests/unit/PaymentHistoryUtilsTest.ts
index e47a90da3777..b5343f8eec0e 100644
--- a/tests/unit/PaymentHistoryUtilsTest.ts
+++ b/tests/unit/PaymentHistoryUtilsTest.ts
@@ -3,6 +3,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider';
import {convertAmountToDisplayString} from '@libs/CurrencyUtils';
import {
formatDate,
+ getActiveUserAccountIDs,
getBadgeText,
getPaymentDetailsBadge,
getPaymentHistoryRow,
@@ -430,3 +431,23 @@ describe('getReceiptFileName', () => {
expect(getReceiptFileName('https://expensify.com/', 7)).toBe('receipt-7');
});
});
+
+describe('getActiveUserAccountIDs', () => {
+ it('dedupes users billed under more than one workspace', () => {
+ // Given a purchase whose active users repeat, because one person was billed under two workspaces
+ const purchase = {...createPurchase(1, '2026-02-01'), activeUsers: [10, 20, 10, 30]};
+
+ // When the active users for the Activity list are read
+ // Then each person is listed once, so the count under the Activity header matches the list
+ expect(getActiveUserAccountIDs(purchase)).toEqual([10, 20, 30]);
+ });
+
+ it('returns no users when the details have not loaded or nobody was billed', () => {
+ // Given a missing purchase, a purchase without active users, and a purchase with an empty list
+ // When the active users are read
+ // Then the list is empty, so the page hides the Activity section
+ expect(getActiveUserAccountIDs(undefined)).toEqual([]);
+ expect(getActiveUserAccountIDs(createPurchase(1, '2026-02-01'))).toEqual([]);
+ expect(getActiveUserAccountIDs({...createPurchase(1, '2026-02-01'), activeUsers: []})).toEqual([]);
+ });
+});