From e603aa41856070e2e690380f3dd018e1ac7e50ad Mon Sep 17 00:00:00 2001 From: Justin Gasper Date: Mon, 4 May 2026 16:29:17 +1000 Subject: [PATCH] PM-4954: Hide copilot billing details modal when flag is off What was broken Copilot users with displayMemberPaymentDetailsToCopilots disabled no longer saw inline member payment balances, but the billing account details icon still rendered on project rows and project detail pages. Root cause The prior PM-4954 fix gated payment amounts with the project flag, but the modal entry points still only checked the global billing details feature flag and a billing account id. What was changed Gate both project-list and project-detail billing account details buttons with the same per-project payment visibility decision used for copilot amounts. Prevent hidden modal entry points from fetching or rendering billing account details when the project flag is off for restricted copilot users. Leave admin, manager, and flag-enabled copilot visibility unchanged. Any added/updated tests Updated ProjectsTable and ProjectBillingAccountExpiredNotice coverage so flag-off copilots no longer see or open the billing account details modal. --- .../ProjectBillingAccountExpiredNotice.spec.tsx | 13 ++++++------- .../ProjectBillingAccountExpiredNotice.tsx | 7 ++++--- .../ProjectsTable/ProjectsTable.spec.tsx | 13 ++++++------- .../components/ProjectsTable/ProjectsTable.tsx | 16 ++++++++++++---- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx index 1f19af869..7e10429d1 100644 --- a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx +++ b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.spec.tsx @@ -229,7 +229,7 @@ describe('ProjectBillingAccountExpiredNotice', () => { .toBeNull() }) - it('hides the inline member payment balance but keeps billing account modal access for copilots ' + it('hides the inline member payment balance and billing account modal access for copilots ' + 'when disabled', () => { mockedUseFetchBillingAccountDetails.mockReturnValue({ billingAccountDetails: { @@ -255,14 +255,13 @@ describe('ProjectBillingAccountExpiredNotice', () => { .toBeNull() expect(screen.queryByText('$750 / $1,000 spent')) .toBeNull() - fireEvent.click(screen.getByRole('button', { + expect(screen.queryByRole('button', { name: 'View billing account details', })) - - expect(screen.getByRole('dialog') - .textContent) - .toContain('Billing account details for 80001063') + .toBeNull() + expect(screen.queryByRole('dialog')) + .toBeNull() expect(mockedUseFetchBillingAccountDetails) - .toHaveBeenCalledWith('80001063') + .toHaveBeenCalledWith(undefined) }) }) diff --git a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx index 96a3c130f..12f2818a2 100644 --- a/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx +++ b/src/apps/work/src/lib/components/ProjectBillingAccountExpiredNotice/ProjectBillingAccountExpiredNotice.tsx @@ -349,6 +349,7 @@ export const ProjectBillingAccountExpiredNotice: FC ) : undefined const billingAccountModal = renderBillingAccountModal( billingAccountDetailsData, - isModalOpen, + showDetailsButton && isModalOpen, handleCloseModal, props.projectId, showMemberPaymentsRemainingInModal, diff --git a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx index 6fe404d86..8870ba76f 100644 --- a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx +++ b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.spec.tsx @@ -320,7 +320,7 @@ describe('ProjectsTable', () => { .toBeNull() }) - it('hides inline payment amounts but keeps billing details available for copilot project rows ' + it('hides inline payment amounts and billing details for copilot project rows ' + 'when disabled', () => { mockedUseFetchBillingAccounts.mockReturnValue({ billingAccounts: [ @@ -366,12 +366,11 @@ describe('ProjectsTable', () => { .toBeNull() expect(screen.queryByText('$750 / $1,000 spent')) .toBeNull() - fireEvent.click(screen.getAllByRole('button', { + expect(screen.queryByRole('button', { name: 'View billing account details', - })[0]) - - expect(screen.getByRole('dialog') - .textContent) - .toContain('Billing account details for 80001063') + })) + .toBeNull() + expect(screen.queryByRole('dialog')) + .toBeNull() }) }) diff --git a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx index 5de3da688..19dfe8896 100644 --- a/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx +++ b/src/apps/work/src/lib/components/ProjectsTable/ProjectsTable.tsx @@ -268,12 +268,16 @@ interface RenderProjectBillingAccountModalParams { * Resolves whether the billing-account details hook should fetch modal data. * * @param isModalOpen Whether the row details modal has been opened. + * @param showDetailsButton Whether this user may open billing-account details. * @returns `true` when the modal feature is enabled and data should be fetched. */ function canFetchProjectBillingAccountDetails( isModalOpen: boolean, + showDetailsButton: boolean, ): boolean { - return BILLING_ACCOUNT_DETAILS_MODAL_ENABLED && isModalOpen + return BILLING_ACCOUNT_DETAILS_MODAL_ENABLED + && showDetailsButton + && isModalOpen } /** @@ -350,13 +354,15 @@ function renderProjectBillingAccountBudget( * * @param billingAccountId Normalized billing-account id for the current row. * @param onOpen Open handler for the row modal. + * @param showDetailsButton Whether this user may open billing-account details. * @returns The details button, or `undefined` when unavailable. */ function renderProjectBillingAccountDetailsButton( billingAccountId: string | undefined, onOpen: () => void, + showDetailsButton: boolean, ): JSX.Element | undefined { - if (!BILLING_ACCOUNT_DETAILS_MODAL_ENABLED || !billingAccountId) { + if (!BILLING_ACCOUNT_DETAILS_MODAL_ENABLED || !showDetailsButton || !billingAccountId) { return undefined } @@ -413,8 +419,9 @@ const ProjectBillingAccountCell: FC = ( const [isModalOpen, setIsModalOpen] = useState(false) const normalizedBillingAccountId = normalizeOptionalString(props.project.billingAccountId) || normalizeOptionalString(props.billingAccount?.id) + const showDetailsButton = props.showPaymentAmounts const billingAccountDetailsResult: UseFetchBillingAccountDetailsResult = useFetchBillingAccountDetails( - canFetchProjectBillingAccountDetails(isModalOpen) + canFetchProjectBillingAccountDetails(isModalOpen, showDetailsButton) ? normalizedBillingAccountId : undefined, ) @@ -438,10 +445,11 @@ const ProjectBillingAccountCell: FC = ( const billingAccountDetailsButton = renderProjectBillingAccountDetailsButton( normalizedBillingAccountId, handleOpenModal, + showDetailsButton, ) const billingAccountModal = renderProjectBillingAccountModal({ billingAccountDetails: billingAccountDetailsResult.billingAccountDetails, - isModalOpen, + isModalOpen: showDetailsButton && isModalOpen, onClose: handleCloseModal, projectId: props.project.id, showMemberPaymentsRemaining: props.showMemberPaymentsRemainingInModal,