diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ee0b415a6cb5..4c50491c60fb 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3914,6 +3914,7 @@ const CONST = { AMEX_DIRECT: 'oauth.americanexpressfdx.com', AMEX_FILE_DOWNLOAD: 'americanexpressfd.us', CSV: 'ccupload', + CSV_CLASSIC: 'csv', MOCK_BANK: 'oauth.mockbank.com', UPLOAD: 'upload', }, diff --git a/src/hooks/useCardFeeds.tsx b/src/hooks/useCardFeeds.tsx index bfc319db8338..6596f80851ce 100644 --- a/src/hooks/useCardFeeds.tsx +++ b/src/hooks/useCardFeeds.tsx @@ -50,8 +50,9 @@ const useCardFeeds = (policyID: string | undefined): [CombinedCardFeeds | undefi let workspaceFeeds: CombinedCardFeeds | undefined; if (policyID && allFeeds) { const shouldIncludeFeedPredicate = (combinedCardFeed: CombinedCardFeed) => { - if (combinedCardFeed?.linkedPolicyIDs) { - return combinedCardFeed.linkedPolicyIDs.includes(policyID); + const validLinkedPolicyIDs = combinedCardFeed?.linkedPolicyIDs?.filter(Boolean); + if (validLinkedPolicyIDs?.length) { + return validLinkedPolicyIDs.includes(policyID); } return combinedCardFeed.preferredPolicy ? combinedCardFeed.preferredPolicy === policyID : combinedCardFeed.domainID === effectiveWorkspaceAccountID; }; diff --git a/src/libs/CardFeedUtils.ts b/src/libs/CardFeedUtils.ts index fd4b7a26ac92..276d69272c94 100644 --- a/src/libs/CardFeedUtils.ts +++ b/src/libs/CardFeedUtils.ts @@ -21,6 +21,7 @@ import { isCard, isCardClosed, isCardHiddenFromSearch, + isCSVUploadFeed, isCustomFeed, isDirectFeed, isPersonalCard, @@ -628,12 +629,14 @@ function getCombinedCardFeedsFromAllFeeds( // When we have card data, filter out stale feeds: // - Direct feeds without oAuthAccountDetails AND no assigned cards - // - "Gray zone" feeds (not commercial, not direct) without assigned cards + // - "Gray zone" feeds (not commercial, not direct, not CSV upload) without assigned cards + // CSV upload feeds are always shown when they exist in settings, since their + // unassigned cards are loaded on-demand when the feed is selected. if (feedKeysWithCards) { if (isDirectFeed(feedName) && !oAuthAccountDetails && !feedHasCards(feedName, domainID, feedKeysWithCards)) { continue; } - if (!isCustomFeed(feedName) && !isDirectFeed(feedName) && !feedHasCards(feedName, domainID, feedKeysWithCards)) { + if (!isCustomFeed(feedName) && !isDirectFeed(feedName) && !isCSVUploadFeed(feedName) && !feedHasCards(feedName, domainID, feedKeysWithCards)) { continue; } } @@ -641,7 +644,7 @@ function getCombinedCardFeedsFromAllFeeds( const combinedCardFeed: CombinedCardFeed = { ...feedSettings, ...oAuthAccountDetails, - customFeedName, + customFeedName: customFeedName ?? feedSettings?.uploadLayoutSettings?.layoutName, domainID, feed: feedName, status, diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index da8b22c03428..be335ae080eb 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -542,13 +542,24 @@ function isCustomFeed(feed: string | undefined): boolean { return CUSTOM_FEED_PREFIXES.some((value) => feed.startsWith(value)); } +/** + * Checks if a feed is a CSV upload feed (ccupload or csv prefix). + * Covers both NewDot-created feeds (ccupload*) and Classic-created feeds (csv*). + */ +function isCSVUploadFeed(feed: string | undefined): boolean { + if (!feed) { + return false; + } + const lowerFeed = feed.toLowerCase(); + return lowerFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV) || lowerFeed.startsWith(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV_CLASSIC); +} + /** * Checks if a feed key represents a CSV feed or Expensify Card. * CSV feeds from Classic and Expensify Cards should not count toward the feed limit for Collect plan workspaces. */ function isCSVFeedOrExpensifyCard(feedKey: string): boolean { - const lowerFeedKey = feedKey.toLowerCase(); - return lowerFeedKey.startsWith('csv') || lowerFeedKey.includes(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV) || feedKey === CONST.EXPENSIFY_CARD.BANK; + return isCSVUploadFeed(feedKey) || feedKey === CONST.EXPENSIFY_CARD.BANK; } /** @@ -1725,6 +1736,7 @@ export { hasCompanyCardFeeds, isPersonalCardBrokenConnection, isCustomFeed, + isCSVUploadFeed, isCSVFeedOrExpensifyCard, getBankCardDetailsImage, getSelectedFeed, diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index 21516fd27ce7..71d0c7d78243 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -119,6 +119,13 @@ type CustomCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Plaid access token */ plaidAccessToken?: string; + /** CSV upload layout settings (present on ccupload feeds) */ + uploadLayoutSettings?: { + /** User-defined name for the CSV upload layout */ + layoutName?: string; + [key: string]: unknown; + }; + /** Field-specific error messages */ errorFields?: OnyxCommon.ErrorFields<'statementPeriodEndDay'>; diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 0ff71304e7c3..3eb82c573f76 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -52,6 +52,7 @@ import { isCardAlreadyAssigned, isCardFrozen, isCSVFeedOrExpensifyCard, + isCSVUploadFeed, isCustomFeed as isCustomFeedCardUtils, isDirectFeed as isDirectFeedCardUtils, isExpensifyCard, @@ -1319,6 +1320,51 @@ describe('CardUtils', () => { }); }); + describe('isCSVUploadFeed', () => { + it('Should return true for ccupload feed', () => { + expect(isCSVUploadFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.CSV)).toBe(true); + }); + + it('Should return true for ccupload feed with number suffix', () => { + expect(isCSVUploadFeed('ccupload1')).toBe(true); + expect(isCSVUploadFeed('ccupload4')).toBe(true); + }); + + it('Should return true for Classic csv feed', () => { + expect(isCSVUploadFeed('csv')).toBe(true); + expect(isCSVUploadFeed('csv1')).toBe(true); + }); + + it('Should return true for csv feed key with domain ID', () => { + expect(isCSVUploadFeed('csv#123456')).toBe(true); + expect(isCSVUploadFeed('ccupload1#158')).toBe(true); + }); + + it('Should return true regardless of case', () => { + expect(isCSVUploadFeed('CCUpload1')).toBe(true); + expect(isCSVUploadFeed('CSV1')).toBe(true); + }); + + it('Should return false for direct feeds', () => { + expect(isCSVUploadFeed('oauth.chase.com')).toBe(false); + expect(isCSVUploadFeed('plaid.ins_19')).toBe(false); + }); + + it('Should return false for custom feeds', () => { + expect(isCSVUploadFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.VISA)).toBe(false); + expect(isCSVUploadFeed(CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD)).toBe(false); + }); + + it('Should return false for Expensify Card', () => { + expect(isCSVUploadFeed(CONST.EXPENSIFY_CARD.BANK)).toBe(false); + }); + + it('Should return false for undefined or empty', () => { + expect(isCSVUploadFeed(undefined)).toBe(false); + expect(isCSVUploadFeed('')).toBe(false); + }); + }); + describe('isCSVFeedOrExpensifyCard', () => { it('Should return true for CSV feed keys', () => { expect(isCSVFeedOrExpensifyCard('csv#123456')).toBe(true);