From b3a3dbca6f6dbb9507deebc89a793472674d152b Mon Sep 17 00:00:00 2001 From: "Maxence Coulibaly (via MelvinBot)" Date: Tue, 5 May 2026 22:36:38 +0000 Subject: [PATCH 1/2] Allow policy admins to assign cards on shared workspaces via linkedPolicyIDs The permission check in useIsAllowedToIssueCompanyCard only allowed card assignment when the feed's domainID matched the current workspace's workspaceAccountID. For feeds shared to other workspaces via linkedPolicyIDs, this check failed, causing the assign button to be greyed out even for workspace admins. This adds a linkedPolicyIDs check so policy admins on any linked workspace can assign cards. Co-authored-by: Maxence Coulibaly --- src/hooks/useIsAllowedToIssueCompanyCard.ts | 2 +- .../useIsAllowedToIssueCompanyCard.test.ts | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/hooks/useIsAllowedToIssueCompanyCard.ts b/src/hooks/useIsAllowedToIssueCompanyCard.ts index 16f35593a1a0..7c906f468742 100644 --- a/src/hooks/useIsAllowedToIssueCompanyCard.ts +++ b/src/hooks/useIsAllowedToIssueCompanyCard.ts @@ -20,7 +20,7 @@ function useIsAllowedToIssueCompanyCard({policyID}: {policyID?: string}) { const selectedFeedData = selectedFeed && companyCards[selectedFeed]; const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${selectedFeedData?.domainID}`); - if (selectedFeedData?.domainID === policy?.workspaceAccountID) { + if (selectedFeedData?.domainID === policy?.workspaceAccountID || selectedFeedData?.linkedPolicyIDs?.includes(policyID ?? '')) { return isPolicyAdmin; } diff --git a/tests/unit/hooks/useIsAllowedToIssueCompanyCard.test.ts b/tests/unit/hooks/useIsAllowedToIssueCompanyCard.test.ts index b1f5ad50ca76..b7d6030c2415 100644 --- a/tests/unit/hooks/useIsAllowedToIssueCompanyCard.test.ts +++ b/tests/unit/hooks/useIsAllowedToIssueCompanyCard.test.ts @@ -14,6 +14,8 @@ const workspaceAccountID = 11111111; const mockPolicy = {...createRandomPolicy(Number(mockPolicyID), CONST.POLICY.TYPE.TEAM, 'TestPolicy'), policyID: mockPolicyID, workspaceAccountID}; +const linkedFeedDomainID = 22222222; + const mockedFeeds = { // eslint-disable-next-line @typescript-eslint/naming-convention 'vcf#19475968': { @@ -31,6 +33,15 @@ const mockedFeeds = { customFeedName: 'Custom feed name 1', feed: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, }, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'vcf#22222222': { + liabilityType: 'personal', + pending: false, + domainID: linkedFeedDomainID, + customFeedName: 'Linked feed', + feed: CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, + linkedPolicyIDs: [mockPolicyID], + }, }; jest.mock('@hooks/useCardFeeds', () => ({ @@ -92,4 +103,22 @@ describe('useIsAllowedToIssueCompanyCard', () => { const {result} = renderHook(() => useIsAllowedToIssueCompanyCard({policyID: mockPolicyID})); expect(result?.current).toBe(false); }); + it('should return true if feed is shared via linkedPolicyIDs and user is admin', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicy?.policyID}`, 'vcf#22222222'); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${mockPolicy?.policyID}`, { + role: CONST.POLICY.ROLE.ADMIN, + }); + (useCardFeeds as jest.Mock).mockReturnValue([mockedFeeds, {status: 'loaded'}]); + const {result} = renderHook(() => useIsAllowedToIssueCompanyCard({policyID: mockPolicyID})); + expect(result?.current).toBe(true); + }); + it('should return false if feed is shared via linkedPolicyIDs and user is not an admin', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${mockPolicy?.policyID}`, 'vcf#22222222'); + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${mockPolicy?.policyID}`, { + role: CONST.POLICY.ROLE.USER, + }); + (useCardFeeds as jest.Mock).mockReturnValue([mockedFeeds, {status: 'loaded'}]); + const {result} = renderHook(() => useIsAllowedToIssueCompanyCard({policyID: mockPolicyID})); + expect(result?.current).toBe(false); + }); }); From 3896482f2591310353fed891de4ec236d74419ab Mon Sep 17 00:00:00 2001 From: "Maxence Coulibaly (via MelvinBot)" Date: Tue, 5 May 2026 22:50:26 +0000 Subject: [PATCH 2/2] Fix: avoid defaulting policyID to empty string to satisfy no-default-id-values lint rule Co-authored-by: Maxence Coulibaly --- src/hooks/useIsAllowedToIssueCompanyCard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useIsAllowedToIssueCompanyCard.ts b/src/hooks/useIsAllowedToIssueCompanyCard.ts index 7c906f468742..ca90d38a94d8 100644 --- a/src/hooks/useIsAllowedToIssueCompanyCard.ts +++ b/src/hooks/useIsAllowedToIssueCompanyCard.ts @@ -20,7 +20,7 @@ function useIsAllowedToIssueCompanyCard({policyID}: {policyID?: string}) { const selectedFeedData = selectedFeed && companyCards[selectedFeed]; const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${selectedFeedData?.domainID}`); - if (selectedFeedData?.domainID === policy?.workspaceAccountID || selectedFeedData?.linkedPolicyIDs?.includes(policyID ?? '')) { + if (selectedFeedData?.domainID === policy?.workspaceAccountID || (policyID && selectedFeedData?.linkedPolicyIDs?.includes(policyID))) { return isPolicyAdmin; }