diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 5c5d0526a5b0..02b9ce5be587 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -11,7 +11,7 @@ import type {OnyxValues} from '@src/ONYXKEYS'; import ONYXKEYS from '@src/ONYXKEYS'; import type {BankAccountList, Card, CardFeeds, CardList, CompanyCardFeed, PersonalDetailsList, WorkspaceCardsList} from '@src/types/onyx'; import type {FilteredCardList} from '@src/types/onyx/Card'; -import type {CompanyCardNicknames, CompanyFeeds, DirectCardFeedData} from '@src/types/onyx/CardFeeds'; +import type {CompanyCardFeedWithNumber, CompanyCardNicknames, CompanyFeeds, DirectCardFeedData} from '@src/types/onyx/CardFeeds'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import localeCompare from './LocaleCompare'; @@ -455,6 +455,28 @@ const getDescriptionForPolicyDomainCard = (domainName: string): string => { return domainName; }; +const CUSTOM_FEEDS = [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD, CONST.COMPANY_CARD.FEED_BANK_NAME.VISA, CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]; + +function getFeedType(feedKey: CompanyCardFeed, cardFeeds: OnyxEntry): CompanyCardFeedWithNumber { + if (CUSTOM_FEEDS.some((feed) => feed === feedKey)) { + const filteredFeeds = Object.keys(cardFeeds?.settings?.companyCards ?? {}).filter((str) => str.includes(feedKey)); + + const feedNumbers = filteredFeeds.map((str) => parseInt(str.replace(feedKey, ''), 10)).filter(Boolean); + feedNumbers.sort((a, b) => a - b); + + let firstAvailableNumber = 1; + for (const num of feedNumbers) { + if (num && num !== firstAvailableNumber) { + return `${feedKey}${firstAvailableNumber}`; + } + firstAvailableNumber++; + } + + return `${feedKey}${firstAvailableNumber}`; + } + return feedKey; +} + export { isExpensifyCard, isCorporateCard, @@ -490,4 +512,5 @@ export { getAllCardsForWorkspace, isCardIssued, isCardHiddenFromSearch, + getFeedType, }; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index d0e19398c257..d4a905d5ef6d 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -1,5 +1,5 @@ +import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {OnyxUpdate} from 'react-native-onyx'; import * as API from '@libs/API'; import type { AssignCompanyCardParams, @@ -18,7 +18,7 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card} from '@src/types/onyx'; +import type {Card, CardFeeds} from '@src/types/onyx'; import type {AssignCard, AssignCardData} from '@src/types/onyx/AssignCard'; import type {AddNewCardFeedData, AddNewCardFeedStep, CompanyCardFeed} from '@src/types/onyx/CardFeeds'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -53,19 +53,36 @@ function clearAddNewCardFlow() { }); } -function addNewCompanyCardsFeed(policyID: string, feedType: CompanyCardFeed, feedDetails: string, lastSelectedFeed?: CompanyCardFeed) { +function addNewCompanyCardsFeed(policyID: string, cardFeed: CompanyCardFeed, feedDetails: string, cardFeeds: OnyxEntry, lastSelectedFeed?: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); + const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); if (!authToken) { return; } + const feedType = CardUtils.getFeedType(cardFeed, cardFeeds); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, value: feedType, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, + value: { + isLoading: true, + settings: { + companyCards: { + [feedType]: { + errors: null, + }, + }, + }, + }, + }, ]; const failureData: OnyxUpdate[] = [ @@ -74,6 +91,18 @@ function addNewCompanyCardsFeed(policyID: string, feedType: CompanyCardFeed, fee key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, value: lastSelectedFeed ?? null, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, + value: { + isLoading: true, + settings: { + companyCards: { + [feedType]: null, + }, + }, + }, + }, ]; const successData: OnyxUpdate[] = [ diff --git a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx index 368a1ef2a6a7..c78b15244a2a 100644 --- a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx +++ b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx @@ -15,10 +15,11 @@ import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ValidationUtils from '@libs/ValidationUtils'; +import {getWorkspaceAccountID} from '@libs/PolicyUtils'; +import {getFieldRequiredErrors} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; -import * as CompanyCards from '@userActions/CompanyCards'; +import {addNewCompanyCardsFeed, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -34,8 +35,13 @@ function DetailsStep({policyID}: DetailsStepProps) { const theme = useTheme(); const styles = useThemeStyles(); const {inputCallbackRef} = useAutoFocusInput(); + const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`); + + const workspaceAccountID = getWorkspaceAccountID(policyID); + const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const feedProvider = addNewCard?.data?.feedType; const isStripeFeedProvider = feedProvider === CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE; const bank = addNewCard?.data?.selectedBank; @@ -53,21 +59,21 @@ function DetailsStep({policyID}: DetailsStepProps) { .map(([key, value]) => `${key}: ${value}`) .join(', '); - CompanyCards.addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, feedDetails, lastSelectedFeed); + addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, feedDetails, cardFeeds, lastSelectedFeed); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); }; const handleBackButtonPress = () => { if (isOtherBankSelected) { - CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_NAME}); + setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_NAME}); return; } - CompanyCards.setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS}); + setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS}); }; const validate = useCallback( (values: FormOnyxValues): FormInputErrors => { - const errors = ValidationUtils.getFieldRequiredErrors(values, [INPUT_IDS.BANK_ID]); + const errors = getFieldRequiredErrors(values, [INPUT_IDS.BANK_ID]); switch (feedProvider) { case CONST.COMPANY_CARD.FEED_BANK_NAME.VISA: diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index 9ea014c5007c..fa3929ac5b6e 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -5,6 +5,9 @@ import type * as OnyxCommon from './OnyxCommon'; /** Card feed */ type CompanyCardFeed = ValueOf; +/** Custom card feed with a number */ +type CompanyCardFeedWithNumber = CompanyCardFeed | `${CompanyCardFeed}${number}`; + /** Card feed provider */ type CardFeedProvider = | typeof CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD @@ -135,4 +138,5 @@ export type { CardFeedProvider, CompanyFeeds, CompanyCardNicknames, + CompanyCardFeedWithNumber, }; diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index f9b7f05dab98..0ac8de13c8c8 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -23,6 +23,22 @@ const companyCardsCustomFeedSettings = { liabilityType: 'personal', }, }; +const companyCardsCustomFeedSettingsWithNumbers = { + [`${CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD}1`]: { + pending: true, + }, + [`${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}1`]: { + liabilityType: 'personal', + }, +}; +const companyCardsCustomVisaFeedSettingsWithNumbers = { + [`${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}1`]: { + pending: false, + }, + [`${CONST.COMPANY_CARD.FEED_BANK_NAME.VISA}3`]: { + pending: false, + }, +}; const companyCardsCustomFeedSettingsWithoutExpensifyBank = { [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: { pending: true, @@ -158,6 +174,18 @@ const cardFeedsCollection: OnyxCollection = { companyCards: companyCardsCustomFeedSettings, }, }, + // Policy with custom feeds only, feed names with numbers + FAKE_ID_4: { + settings: { + companyCards: companyCardsCustomFeedSettingsWithNumbers, + }, + }, + // Policy with several Visa feeds + FAKE_ID_5: { + settings: { + companyCards: companyCardsCustomVisaFeedSettingsWithNumbers, + }, + }, }; describe('CardUtils', () => { @@ -355,8 +383,12 @@ describe('CardUtils', () => { describe('getFilteredCardList', () => { it('Should return filtered custom feed cards list', () => { const cardsList = CardUtils.getFilteredCardList(customFeedCardsList, undefined); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(cardsList).toStrictEqual({'480801XXXXXX2111': 'ENCRYPTED_CARD_NUMBER', '480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER'}); + expect(cardsList).toStrictEqual({ + // eslint-disable-next-line @typescript-eslint/naming-convention + '480801XXXXXX2111': 'ENCRYPTED_CARD_NUMBER', + // eslint-disable-next-line @typescript-eslint/naming-convention + '480801XXXXXX2566': 'ENCRYPTED_CARD_NUMBER', + }); }); it('Should return filtered direct feed cards list with a single card', () => { @@ -367,8 +399,14 @@ describe('CardUtils', () => { it('Should return filtered direct feed cards list with multiple cards', () => { const cardsList = CardUtils.getFilteredCardList(directFeedCardsMultipleList, oAuthAccountDetails[CONST.COMPANY_CARD.FEED_BANK_NAME.CAPITAL_ONE]); - // eslint-disable-next-line @typescript-eslint/naming-convention - expect(cardsList).toStrictEqual({'CREDIT CARD...1233': 'CREDIT CARD...1233', 'CREDIT CARD...3333': 'CREDIT CARD...3333', 'CREDIT CARD...7788': 'CREDIT CARD...7788'}); + expect(cardsList).toStrictEqual({ + // eslint-disable-next-line @typescript-eslint/naming-convention + 'CREDIT CARD...1233': 'CREDIT CARD...1233', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'CREDIT CARD...3333': 'CREDIT CARD...3333', + // eslint-disable-next-line @typescript-eslint/naming-convention + 'CREDIT CARD...7788': 'CREDIT CARD...7788', + }); }); it('Should return empty object if no data was provided', () => { @@ -376,4 +414,21 @@ describe('CardUtils', () => { expect(cardsList).toStrictEqual({}); }); }); + + describe('getFeedType', () => { + it('should return the feed name with a consecutive number, if there is already a feed with a number', () => { + const feedType = CardUtils.getFeedType('vcf', cardFeedsCollection.FAKE_ID_4); + expect(feedType).toBe('vcf2'); + }); + + it('should return the feed name with 1, if there is already a feed without a number', () => { + const feedType = CardUtils.getFeedType('vcf', cardFeedsCollection.FAKE_ID_3); + expect(feedType).toBe('vcf1'); + }); + + it('should return the feed name with with the first smallest available number', () => { + const feedType = CardUtils.getFeedType('vcf', cardFeedsCollection.FAKE_ID_5); + expect(feedType).toBe('vcf2'); + }); + }); });