Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/components/Tables/WorkspaceCompanyCardsTable/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {companyCardCustomNamesSelector} from '@selectors/Card';
import type {ListRenderItemInfo} from '@shopify/flash-list';
import React, {useEffect, useRef, useState} from 'react';
import {View} from 'react-native';
Expand All @@ -19,7 +20,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {resetFailedWorkspaceCompanyCardUnassignment} from '@libs/actions/CompanyCards';
import {getDefaultCardName} from '@libs/CardUtils';
import {getCompanyCardCustomName, getDefaultCardName} from '@libs/CardUtils';
import tokenizedSearch from '@libs/tokenizedSearch';
import WorkspaceCompanyCardPageEmptyState from '@pages/workspace/companyCards/WorkspaceCompanyCardPageEmptyState';
import WorkspaceCompanyCardsFeedAddedEmptyPage from '@pages/workspace/companyCards/WorkspaceCompanyCardsFeedAddedEmptyPage';
Expand Down Expand Up @@ -100,6 +101,7 @@ function WorkspaceCompanyCardsTable({
const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY);
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const [personalDetails, personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [sharedCardCustomNames] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`, {selector: companyCardCustomNamesSelector});

const hasNoAssignedCard = Object.keys(assignedCards ?? {}).length === 0;
const areWorkspaceCardFeedsLoading = !!workspaceCardFeedsStatus?.[domainOrWorkspaceAccountID]?.isLoading;
Expand Down Expand Up @@ -184,8 +186,7 @@ function WorkspaceCompanyCardsTable({
cardName,
keyForList: `${cardName}_${assignedCard?.cardID ?? 'unassigned'}_${encryptedCardNumber}`,
encryptedCardNumber,
customCardName:
assignedCard?.cardID && customCardNames?.[assignedCard.cardID] ? customCardNames?.[assignedCard.cardID] : getDefaultCardName(cardholder?.displayName ?? ''),
customCardName: getCompanyCardCustomName(assignedCard?.cardID, sharedCardCustomNames, customCardNames) ?? getDefaultCardName(cardholder?.displayName ?? ''),
isCardDeleted: assignedCard?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
isAssigned,
assignedCard,
Expand Down
13 changes: 13 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,18 @@ function getDefaultCardName(cardholder?: string) {
return `${cardholder}'s card`;
}

/** Resolves a company card's custom name, preferring the shared workspace NVP over the personal NVP. */
function getCompanyCardCustomName(
cardID: string | number | undefined,
sharedCardCustomNames: OnyxEntry<Record<string, string>>,
customCardNames: OnyxEntry<Record<string, string>>,
): string | undefined {
if (!cardID) {
return undefined;
}
return sharedCardCustomNames?.[cardID] ?? customCardNames?.[cardID];
}

/** Returns the date option for a card assignment — CUSTOM when not editing, or the existing option when editing. */
function getCardAssignmentDateOption(isEditing: boolean | undefined, existingDateOption?: string): ValueOf<typeof CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS> {
if (!isEditing) {
Expand Down Expand Up @@ -1944,6 +1956,7 @@ export {
hasOnlyOneCardToAssign,
checkIfNewFeedConnected,
getDefaultCardName,
getCompanyCardCustomName,
getCardAssignmentDateOption,
getCardAssignmentStartDate,
getDomainOrWorkspaceAccountID,
Expand Down
18 changes: 16 additions & 2 deletions src/libs/actions/CompanyCards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,9 @@ function updateWorkspaceCompanyCard(domainOrWorkspaceAccountID: number, cardID:
}

function updateCompanyCardName(domainOrWorkspaceAccountID: number, cardID: string, newCardTitle: string, bankName: CompanyCardFeedWithNumber, oldCardTitle?: string) {
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST | typeof ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES>> = [
const optimisticData: Array<
OnyxUpdate<typeof ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST | typeof ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES | typeof ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER>
> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${domainOrWorkspaceAccountID}_${bankName}`,
Expand All @@ -720,6 +722,11 @@ function updateCompanyCardName(domainOrWorkspaceAccountID: number, cardID: strin
key: ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES,
value: {[cardID]: newCardTitle},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`,
value: {settings: {companyCardCustomNames: {[cardID]: newCardTitle}}},
},
];

const finallyData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST>> = [
Expand All @@ -737,7 +744,9 @@ function updateCompanyCardName(domainOrWorkspaceAccountID: number, cardID: strin
},
},
];
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST | typeof ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES>> = [
const failureData: Array<
OnyxUpdate<typeof ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST | typeof ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES | typeof ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER>
> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${domainOrWorkspaceAccountID}_${bankName}`,
Expand All @@ -759,6 +768,11 @@ function updateCompanyCardName(domainOrWorkspaceAccountID: number, cardID: strin
key: ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES,
value: {[cardID]: oldCardTitle},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`,
value: {settings: {companyCardCustomNames: {[cardID]: oldCardTitle ?? null}}},
},
];

const parameters: UpdateCompanyCardNameParams = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {companyCardCustomNamesSelector} from '@selectors/Card';
import {format, parseISO} from 'date-fns';
import React, {useState} from 'react';
import {View} from 'react-native';
Expand Down Expand Up @@ -28,6 +29,7 @@ import navigateToCardTransactions from '@libs/CardNavigationUtils';
import {
getCardFeedIcon,
getCardFeedTextColor,
getCompanyCardCustomName,
getCompanyCardFeed,
getCompanyFeeds,
getDefaultCardName,
Expand Down Expand Up @@ -101,6 +103,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag

const companyFeeds = getCompanyFeeds(cardFeeds);
const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]);
const [sharedCardCustomNames] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`, {selector: companyCardCustomNamesSelector});
const plaidUrl = getPlaidInstitutionIconUrl(feedName);

// Show "Break connection" only when Mock Bank requests target non-production APIs.
Expand Down Expand Up @@ -211,7 +214,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
>
<MenuItemWithTopDescription
description={translate('workspace.moreFeatures.companyCards.cardName')}
title={customCardNames?.[cardID] ?? getDefaultCardName(cardholder?.displayName)}
title={getCompanyCardCustomName(cardID, sharedCardCustomNames, customCardNames) ?? getDefaultCardName(cardholder?.displayName)}
shouldShowRightIcon={canWriteCompanyCards}
brickRoadIndicator={card?.nameValuePairs?.errorFields?.cardTitle ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARD_EDIT_CARD_NAME.getRoute(policyID, cardID, feedName))}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {companyCardCustomNamesSelector} from '@selectors/Card';
import React from 'react';
import FormProvider from '@components/Form/FormProvider';
import InputWrapper from '@components/Form/InputWrapper';
Expand All @@ -12,7 +13,7 @@ import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';
import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils';
import {getCompanyCardCustomName, getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID} from '@libs/CardUtils';
import {addErrorMessage} from '@libs/ErrorUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {getFieldRequiredErrors, isValidInputLength} from '@libs/ValidationUtils';
Expand All @@ -33,16 +34,20 @@ function WorkspaceCompanyCardEditCardNamePage({route}: WorkspaceCompanyCardEditC
const {policyID, cardID, feed} = route.params;
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [customCardNames, customCardNamesMetadata] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const defaultValue = customCardNames?.[cardID];

const {translate} = useLocalize();
const {inputCallbackRef} = useAutoFocusInput();
const styles = useThemeStyles();

const [cardFeeds] = useCardFeeds(policyID);
const [cardFeeds, cardFeedsMetadata] = useCardFeeds(policyID);
const companyFeeds = getCompanyFeeds(cardFeeds);
const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feed]);

const [sharedCardCustomNames, sharedCardCustomNamesMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainOrWorkspaceAccountID}`, {
selector: companyCardCustomNamesSelector,
});
const defaultValue = getCompanyCardCustomName(cardID, sharedCardCustomNames, customCardNames);

const submit = (values: FormOnyxValues<typeof ONYXKEYS.FORMS.EDIT_WORKSPACE_COMPANY_CARD_NAME_FORM>) => {
updateCompanyCardName(domainOrWorkspaceAccountID, cardID, values[INPUT_IDS.NAME], getCompanyCardFeed(feed), defaultValue);
Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, feed, cardID), {compareParams: false});
Expand All @@ -59,7 +64,7 @@ function WorkspaceCompanyCardEditCardNamePage({route}: WorkspaceCompanyCardEditC
return errors;
};

if (isLoadingOnyxValue(customCardNamesMetadata)) {
if (isLoadingOnyxValue(customCardNamesMetadata, sharedCardCustomNamesMetadata, cardFeedsMetadata)) {
return null;
}

Expand Down
6 changes: 5 additions & 1 deletion src/selectors/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {isCard, isCardHiddenFromSearch, isCSVFeedOrExpensifyCard, isExpensifyCar
import {filterObject} from '@libs/ObjectUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {CardList, NonPersonalAndWorkspaceCardListDerivedValue, WorkspaceCardsList} from '@src/types/onyx';
import type {CardFeeds, CardList, NonPersonalAndWorkspaceCardListDerivedValue, WorkspaceCardsList} from '@src/types/onyx';

/**
* Builds a lightweight map of "${domainID}_${feedName}" keys that have card entries.
Expand Down Expand Up @@ -101,6 +101,9 @@ const isExpensifyCardContinuousReconciliationEnabledSelector = (value: boolean |
return typeof value === 'string' ? value === '1' : value;
};

/** Picks the shared company card custom names from a domain's card feeds, avoiding a subscription to the entire CardFeeds object. */
const companyCardCustomNamesSelector = (cardFeeds: OnyxEntry<CardFeeds>) => cardFeeds?.settings?.companyCardCustomNames;

export {
filterCardsHiddenFromSearch,
filterOutPersonalCards,
Expand All @@ -111,4 +114,5 @@ export {
isExpensifyCardUkEuSupportedSelector,
getBankLinkedPersonalCards,
isExpensifyCardContinuousReconciliationEnabledSelector,
companyCardCustomNamesSelector,
};
3 changes: 3 additions & 0 deletions src/types/onyx/CardFeeds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ type CardFeeds = {
/** User-friendly feed nicknames */
companyCardNicknames?: Partial<Record<CardFeedWithNumber, string>>;

/** Custom card names by card ID */
companyCardCustomNames?: Record<string, string>;

/** Company cards feeds */
companyCards?: Partial<Record<CardFeedWithNumber, CustomCardFeedData>>;

Expand Down
22 changes: 22 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
getCardHintText,
getCardsByCardholderName,
getCardSettings,
getCompanyCardCustomName,
getCompanyCardDescription,
getCompanyCardFeed,
getCompanyFeeds,
Expand Down Expand Up @@ -4637,3 +4638,24 @@ describe('CardArtworkColors drift detection', () => {
expect(CARD_FEED_COLORS[key].background).toBe(actual);
});
});

describe('getCompanyCardCustomName', () => {
const sharedCardCustomNames = {'1234': 'Shared name'};
const customCardNames = {'1234': 'Personal name', '5678': 'Other personal name'};

it('returns undefined when cardID is not provided', () => {
expect(getCompanyCardCustomName(undefined, sharedCardCustomNames, customCardNames)).toBeUndefined();
});

it('prefers the shared NVP name over the personal NVP name', () => {
expect(getCompanyCardCustomName('1234', sharedCardCustomNames, customCardNames)).toBe('Shared name');
});

it('falls back to the personal NVP name when the shared NVP has no entry', () => {
expect(getCompanyCardCustomName('5678', sharedCardCustomNames, customCardNames)).toBe('Other personal name');
});

it('returns undefined when neither NVP has a name for the card', () => {
expect(getCompanyCardCustomName('9999', sharedCardCustomNames, customCardNames)).toBeUndefined();
});
});
Loading