diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index d7da1da78f50..766126a81435 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -20,6 +20,12 @@ import type {ConnectionName, SageIntacctMappingName} from './types/onyx/Policy';
import type {CustomFieldType} from './types/onyx/PolicyEmployee';
import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual';
+type WorkspaceCompanyCardsAssignCardParams = {
+ policyID: string;
+ feed: string;
+ cardID?: string;
+};
+
// This is a file containing constants for all the routes we want to be able to go to
/**
@@ -2171,10 +2177,14 @@ const ROUTES = {
getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/select-feed` as const,
},
WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: {
- route: 'workspaces/:policyID/company-cards/:feed/assign-card',
+ route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID',
- // eslint-disable-next-line no-restricted-syntax -- Legacy route generation
- getRoute: (policyID: string, feed: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/assign-card`, backTo),
+ getRoute: (params: WorkspaceCompanyCardsAssignCardParams, backTo?: string) =>
+ // eslint-disable-next-line no-restricted-syntax -- Legacy route generation
+ getUrlWithBackToParam(
+ `workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card${params.cardID ? `/${encodeURIComponent(params.cardID)}` : ''}`,
+ backTo,
+ ),
},
WORKSPACE_COMPANY_CARD_DETAILS: {
route: 'workspaces/:policyID/company-cards/:bank/:cardID',
diff --git a/src/hooks/useAssignCard.ts b/src/hooks/useAssignCard.ts
index 91b3a147a976..cd70b6231b41 100644
--- a/src/hooks/useAssignCard.ts
+++ b/src/hooks/useAssignCard.ts
@@ -74,7 +74,11 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA
const isAssigningCardDisabled = !currentFeedData || !!currentFeedData?.pending || isSelectedFeedConnectionBroken || !isAllowedToIssueCompanyCard;
- const assignCard = () => {
+ const assignCard = (cardID?: string) => {
+ if (isAssigningCardDisabled) {
+ return;
+ }
+
if (isActingAsDelegate) {
showDelegateNoAccessModal();
return;
@@ -97,6 +101,10 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA
bankName: feed,
};
+ if (cardID) {
+ data.encryptedCardNumber = cardID;
+ }
+
let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.ASSIGNEE;
const employeeList = Object.values(policy?.employeeList ?? {}).filter((employee) => !isDeletedPolicyEmployee(employee, isOffline));
const isFeedExpired = isSelectedFeedExpired(selectedFeedData);
@@ -108,7 +116,7 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA
importPlaidAccounts('', selectedFeed, '', country, getDomainNameForPolicy(policyID), '', undefined, undefined, plaidAccessToken);
}
- if (employeeList.length === 1) {
+ if (!cardID && employeeList.length === 1) {
const userEmail = Object.keys(policy?.employeeList ?? {}).at(0) ?? '';
data.email = userEmail;
const personalDetails = getPersonalDetailByEmail(userEmail);
@@ -138,7 +146,9 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA
clearAddNewCardFlow();
setAssignCardStepAndData({data, currentStep});
- Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed)));
+ Navigation.setNavigationActionToMicrotaskQueue(() => {
+ Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed, cardID}));
+ });
};
return {
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 957c170387a6..d18cbbe54200 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -4851,6 +4851,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU
feedName: (feedName: string) => `${feedName}-Karten`,
directFeed: 'Direkt-Feed',
whoNeedsCardAssigned: 'Wer braucht eine zugewiesene Karte?',
+ chooseTheCardholder: 'Wähle den Karteninhaber',
chooseCard: 'Wähle eine Karte',
chooseCardFor: (assignee: string) =>
`Wähle eine Karte für ${assignee}. Du findest die Karte, die du suchst, nicht? Teile es uns mit.`,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 63950ad2df8b..29fba3b40c42 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -4736,13 +4736,15 @@ const translations = {
feedName: (feedName: string) => `${feedName} cards`,
directFeed: 'Direct feed',
whoNeedsCardAssigned: 'Who needs a card assigned?',
+ chooseTheCardholder: 'Choose the cardholder',
chooseCard: 'Choose a card',
chooseCardFor: (assignee: string) => `Choose a card for ${assignee}. Can't find the card you're looking for? Let us know.`,
noActiveCards: 'No active cards on this feed',
somethingMightBeBroken:
'Or something might be broken. Either way, if you have any questions, just contact Concierge.',
chooseTransactionStartDate: 'Choose a transaction start date',
- startDateDescription: "We'll import all transactions from this date onwards. If no date is specified, we’ll go as far back as your bank allows.",
+ startDateDescription: "Choose your import start date. We'll sync all transactions from this date onwards.",
+
fromTheBeginning: 'From the beginning',
customStartDate: 'Custom start date',
customCloseDate: 'Custom close date',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 912154c2627d..b117a30ef4ce 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -4420,6 +4420,7 @@ ${amount} para ${merchant} - ${date}`,
feedName: (feedName) => `Tarjetas ${feedName}`,
directFeed: 'Fuente directa',
whoNeedsCardAssigned: '¿Quién necesita una tarjeta?',
+ chooseTheCardholder: 'Elige el titular de la tarjeta',
chooseCard: 'Elige una tarjeta',
chooseCardFor: (assignee) => `Elige una tarjeta para ${assignee}. ¿No encuentras la tarjeta que buscas? Avísanos.`,
noActiveCards: 'No hay tarjetas activas en este feed',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 64ec563648a4..38ac463dd200 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -4856,6 +4856,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST.
feedName: (feedName: string) => `Cartes ${feedName}`,
directFeed: 'Flux direct',
whoNeedsCardAssigned: 'Qui a besoin d’une carte attribuée ?',
+ chooseTheCardholder: 'Choisissez le titulaire de la carte',
chooseCard: 'Choisissez une carte',
chooseCardFor: (assignee: string) =>
`Choisissez une carte pour ${assignee}. Vous ne trouvez pas la carte que vous recherchez ? Faites-le-nous savoir.`,
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 8f7ceafd6b05..76f274afcbad 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -4834,6 +4834,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST.
feedName: (feedName: string) => `Carte ${feedName}`,
directFeed: 'Feed diretto',
whoNeedsCardAssigned: 'Chi ha bisogno di una carta assegnata?',
+ chooseTheCardholder: 'Scegli il titolare della carta',
chooseCard: 'Scegli una carta',
chooseCardFor: (assignee: string) =>
`Scegli una carta per ${assignee}. Non riesci a trovare la carta che stai cercando? Facci sapere.`,
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 7d89a81a11a5..68fa60ae3c0f 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -4809,6 +4809,7 @@ _より詳しい手順については、[ヘルプサイトをご覧ください
feedName: (feedName: string) => `${feedName} カード`,
directFeed: 'ダイレクトフィード',
whoNeedsCardAssigned: '誰にカードを割り当てる必要がありますか?',
+ chooseTheCardholder: 'カード所有者を選択',
chooseCard: 'カードを選択',
chooseCardFor: (assignee: string) =>
`${assignee} に使うカードを選択してください。お探しのカードが見つかりませんか?お知らせください。`,
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index b09da4a62da8..63be96f36cd1 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -4829,6 +4829,7 @@ _Voor gedetailleerdere instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPO
feedName: (feedName: string) => `${feedName}-kaarten`,
directFeed: 'Directe feed',
whoNeedsCardAssigned: 'Wie heeft een kaart toegewezen nodig?',
+ chooseTheCardholder: 'Kies de kaarthouder',
chooseCard: 'Kies een kaart',
chooseCardFor: (assignee: string) =>
`Kies een kaart voor ${assignee}. Kun je de kaart die je zoekt niet vinden? Laat het ons weten.`,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index fe35edb31688..c483bdca120b 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -4821,6 +4821,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy
feedName: (feedName: string) => `Karty ${feedName}`,
directFeed: 'Bezpośredni kanał',
whoNeedsCardAssigned: 'Kto potrzebuje przypisanej karty?',
+ chooseTheCardholder: 'Wybierz posiadacza karty',
chooseCard: 'Wybierz kartę',
chooseCardFor: (assignee: string) => `Wybierz kartę dla ${assignee}. Nie możesz znaleźć karty, której szukasz? Daj nam znać.`,
noActiveCards: 'Brak aktywnych kart w tym kanale',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 16cf5b6d90e7..d28e78b7c90e 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -4821,6 +4821,7 @@ _Para instruções mais detalhadas, [visite nosso site de ajuda](${CONST.NETSUIT
feedName: (feedName: string) => `Cartões ${feedName}`,
directFeed: 'Conexão direta',
whoNeedsCardAssigned: 'Quem precisa de um cartão atribuído?',
+ chooseTheCardholder: 'Escolha o portador do cartão',
chooseCard: 'Escolha um cartão',
chooseCardFor: (assignee: string) =>
`Escolha um cartão para ${assignee}. Não encontra o cartão que está procurando? Avise-nos.`,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 28acf8fc2717..b8597b316861 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -4739,6 +4739,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM
feedName: (feedName: string) => `${feedName} 卡片`,
directFeed: '直接数据馈送',
whoNeedsCardAssigned: '谁需要被分配一张卡?',
+ chooseTheCardholder: '选择持卡人',
chooseCard: '选择一张卡片',
chooseCardFor: (assignee: string) => `为 ${assignee} 选择一张卡片。找不到您要找的卡片?请告诉我们。`,
noActiveCards: '此信息流中没有有效的卡片',
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 2974734b708c..8ab71d4d025e 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1202,6 +1202,8 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: {
policyID: string;
feed: CompanyCardFeed;
+ cardID?: string;
+
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
backTo?: Routes;
};
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx
new file mode 100644
index 000000000000..67837e662aa3
--- /dev/null
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx
@@ -0,0 +1,252 @@
+import type {FlashListRef, ListRenderItemInfo} from '@shopify/flash-list';
+import {FlashList} from '@shopify/flash-list';
+import React, {useRef} from 'react';
+import {View} from 'react-native';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import {PressableWithFeedback} from '@components/Pressable';
+import SearchBar from '@components/SearchBar';
+import TableRowSkeleton from '@components/Skeletons/TableRowSkeleton';
+import Text from '@components/Text';
+import useCardFeeds from '@hooks/useCardFeeds';
+import useCardsList from '@hooks/useCardsList';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useOnyx from '@hooks/useOnyx';
+import usePolicy from '@hooks/usePolicy';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSearchResults from '@hooks/useSearchResults';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {
+ filterCardsByPersonalDetails,
+ getCardsByCardholderName,
+ getCompanyCardFeedWithDomainID,
+ getCompanyFeeds,
+ getPlaidInstitutionIconUrl,
+ getPlaidInstitutionId,
+ isCustomFeed,
+ sortCardsByCardholderName,
+} from '@libs/CardUtils';
+import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils';
+import Navigation from '@navigation/Navigation';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {Card, CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+import WorkspaceCompanyCardsFeedAddedEmptyPage from './WorkspaceCompanyCardsFeedAddedEmptyPage';
+import WorkspaceCompanyCardsListRow from './WorkspaceCompanyCardsListRow';
+
+type WorkspaceCompanyCardsListProps = {
+ /** Selected feed */
+ selectedFeed: CompanyCardFeedWithDomainID;
+
+ /** Current policy id */
+ policyID: string;
+
+ /** On assign card callback */
+ onAssignCard: (cardID?: string) => void;
+
+ /** Whether to disable assign card button */
+ isAssigningCardDisabled?: boolean;
+
+ /** Whether to show GB disclaimer */
+ shouldShowGBDisclaimer?: boolean;
+};
+
+function WorkspaceCompanyCardsList({selectedFeed, policyID, onAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer}: WorkspaceCompanyCardsListProps) {
+ const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+ const {translate, localeCompare} = useLocalize();
+ const listRef = useRef>(null);
+ const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
+
+ const [cardsList, cardsListMetadata] = useCardsList(selectedFeed);
+ const isLoadingCardsList = !isOffline && isLoadingOnyxValue(cardsListMetadata);
+ const [personalDetails, personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
+ const isLoadingPersonalDetails = !isOffline && isLoadingOnyxValue(personalDetailsMetadata);
+ const isLoadingCardsTableData = isLoadingCardsList || isLoadingPersonalDetails;
+
+ const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, {canBeMissing: true});
+ const policy = usePolicy(policyID);
+
+ const cardListTyped: Record | undefined = (cardsList as {cardList?: Record})?.cardList ?? {};
+ const assignedCards = Object.fromEntries(Object.entries(cardsList ?? {}).filter(([key]) => key !== 'cardList')) as Record;
+ const [cardFeeds] = useCardFeeds(policyID);
+
+ const companyFeeds = getCompanyFeeds(cardFeeds);
+ const cards = companyFeeds?.[selectedFeed]?.accountList;
+
+ const plaidIconUrl = getPlaidInstitutionIconUrl(selectedFeed);
+
+ // Get all cards sorted by cardholder name
+ const policyMembersAccountIDs = Object.values(getMemberAccountIDsForWorkspace(policy?.employeeList));
+ const allCards = getCardsByCardholderName(cardsList, policyMembersAccountIDs);
+
+ // Filter and sort cards based on search input
+ const filterCard = (card: Card, searchInput: string) => filterCardsByPersonalDetails(card, searchInput, personalDetails);
+ const sortCards = (cardsToSort: Card[]) => sortCardsByCardholderName(cardsToSort, personalDetails, localeCompare);
+ const [inputValue, setInputValue, filteredSortedCards] = useSearchResults(allCards, filterCard, sortCards);
+
+ const isSearchEmpty = filteredSortedCards.length === 0 && inputValue.length > 0;
+
+ // When we reach the medium screen width or the narrow layout is active,
+ // we want to hide the table header and the middle column of the card rows, so that the content is not overlapping.
+ const shouldUseNarrowTableRowLayout = isMediumScreenWidth || shouldUseNarrowLayout;
+
+ const renderItem = ({item: cardName, index}: ListRenderItemInfo) => {
+ const assignedCard = Object.values(assignedCards ?? {}).find((card) => card.cardName === cardName);
+
+ const customCardName = assignedCard?.cardID ? customCardNames?.[assignedCard.cardID] : undefined;
+
+ const isCardDeleted = assignedCard?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
+
+ let cardIdentifier: string | undefined;
+ if (!assignedCard) {
+ const isPlaid = !!getPlaidInstitutionId(selectedFeed);
+ const isCommercial = isCustomFeed(selectedFeed);
+
+ if (isPlaid) {
+ cardIdentifier = cardName;
+ } else if (isCommercial) {
+ const cardValue = cardListTyped?.[cardName] ?? cardName;
+ const digitsOnly = cardValue.replaceAll(/\D/g, '');
+ if (digitsOnly.length >= 10) {
+ const first6 = digitsOnly.substring(0, 6);
+ const last4 = digitsOnly.substring(digitsOnly.length - 4);
+ cardIdentifier = `${first6}${last4}`;
+ } else {
+ cardIdentifier = cardValue;
+ }
+ } else {
+ cardIdentifier = cardListTyped?.[cardName] ?? cardName;
+ }
+ }
+
+ return (
+
+ {
+ if (!assignedCard) {
+ onAssignCard(cardIdentifier);
+ return;
+ }
+
+ if (!assignedCard?.accountID || !assignedCard?.fundID) {
+ return;
+ }
+
+ return Navigation.navigate(
+ ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(
+ policyID,
+ assignedCard.cardID.toString(),
+ getCompanyCardFeedWithDomainID(assignedCard?.bank as CompanyCardFeed, assignedCard.fundID),
+ ),
+ );
+ }}
+ >
+ {({hovered}) => (
+ onAssignCard(cardIdentifier)}
+ isAssigningCardDisabled={isAssigningCardDisabled}
+ shouldUseNarrowTableRowLayout={shouldUseNarrowTableRowLayout}
+ />
+ )}
+
+
+ );
+ };
+
+ const keyExtractor = (item: string, index: number) => `${item}_${index}`;
+
+ const ListHeaderComponent = shouldUseNarrowTableRowLayout ? (
+
+ ) : (
+ <>
+ {(cards?.length ?? 0) > CONST.SEARCH_ITEM_LIMIT && (
+
+ )}
+ {!isSearchEmpty && (
+
+
+
+ {translate('common.member')}
+
+
+
+
+ {translate('workspace.companyCards.card')}
+
+
+
+
+ {translate('workspace.companyCards.cardName')}
+
+
+
+ )}
+ >
+ );
+
+ // Show empty state when there are no cards
+ if (!cards?.length && !isLoadingCardsTableData) {
+ return (
+
+ );
+ }
+
+ return (
+
+ : undefined}
+ showsVerticalScrollIndicator={false}
+ keyboardShouldPersistTaps="handled"
+ contentContainerStyle={styles.flexGrow1}
+ />
+
+ );
+}
+
+WorkspaceCompanyCardsList.displayName = 'WorkspaceCompanyCardsList';
+
+export default WorkspaceCompanyCardsList;
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
index 7657c8ceeeaa..2de8af437685 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useEffect, useState} from 'react';
import ActivityIndicator from '@components/ActivityIndicator';
import DecisionModal from '@components/DecisionModal';
import useAssignCard from '@hooks/useAssignCard';
@@ -20,7 +20,7 @@ import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActio
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
-import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
import WorkspaceCompanyCardPageEmptyState from './WorkspaceCompanyCardPageEmptyState';
import WorkspaceCompanyCardsFeedPendingPage from './WorkspaceCompanyCardsFeedPendingPage';
import WorkspaceCompanyCardsTable from './WorkspaceCompanyCardsTable';
@@ -41,35 +41,29 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) {
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false});
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true});
-
- const [cardFeeds, , defaultFeed] = useCardFeeds(policyID);
+ const [cardFeeds] = useCardFeeds(policyID);
const selectedFeed = getSelectedFeed(lastSelectedFeed, cardFeeds);
const companyFeeds = getCompanyFeeds(cardFeeds);
const selectedFeedData = selectedFeed && companyFeeds[selectedFeed];
const feed = selectedFeed ? getCompanyCardFeed(selectedFeed) : undefined;
- const [cardsList] = useCardsList(selectedFeed);
-
+ const [cardsList, cardsListMetadata] = useCardsList(selectedFeed);
+ const hasNoAssignedCard = Object.keys(cardsList ?? {}).length === 0;
const isNoFeed = !selectedFeedData;
const isFeedPending = !!selectedFeedData?.pending;
const isFeedAdded = !isFeedPending && !isNoFeed;
-
const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false);
- const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeedData);
+ const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeedData)
+ const {isOffline} = useNetwork({
+ onReconnect: () => openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID),
+ });
+ const isLoading = !isOffline && (!cardFeeds || (isFeedAdded && isLoadingOnyxValue(cardsListMetadata)));
const isGB = countryByIp === CONST.COUNTRY.GB;
- const hasNoAssignedCard = Object.keys(cardsList ?? {}).length === 0;
const shouldShowGBDisclaimer = isGB && isBetaEnabled(CONST.BETAS.PLAID_COMPANY_CARDS) && (isNoFeed || hasNoAssignedCard);
- const fetchCompanyCards = useCallback(() => {
- openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID);
- }, [domainOrWorkspaceAccountID, policyID]);
-
- const {isOffline} = useNetwork({onReconnect: fetchCompanyCards});
- const isLoading = !isOffline && (!cardFeeds || (!!defaultFeed?.isLoading && isEmptyObject(cardsList)));
-
useEffect(() => {
- fetchCompanyCards();
- }, [fetchCompanyCards]);
+ openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID);
+ }, [policyID, domainOrWorkspaceAccountID]);
useEffect(() => {
if (isLoading || !feed || isFeedPending) {
@@ -121,7 +115,6 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) {
{isFeedAdded && !isFeedPending && (
;
-
/** Current policy id */
policyID: string;
/** On assign card callback */
- onAssignCard: () => void;
+ onAssignCard: (cardID?: string) => void;
/** Whether to disable assign card button */
isAssigningCardDisabled?: boolean;
@@ -41,15 +41,22 @@ type WorkspaceCompanyCardsTableProps = {
shouldShowGBDisclaimer?: boolean;
};
-function WorkspaceCompanyCardsTable({selectedFeed, cardsList, policyID, onAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer}: WorkspaceCompanyCardsTableProps) {
+function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer}: WorkspaceCompanyCardsTableProps) {
const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
const {translate, localeCompare} = useLocalize();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
- const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
+ const [cardsList, cardsListMetadata] = useCardsList(selectedFeed);
+ const isLoadingCardsList = !isOffline && isLoadingOnyxValue(cardsListMetadata);
+ const [personalDetails, personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false});
+ const isLoadingPersonalDetails = !isOffline && isLoadingOnyxValue(personalDetailsMetadata);
+ const isLoadingCardsTableData = isLoadingCardsList || isLoadingPersonalDetails;
+
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES, {canBeMissing: true});
- const {cardList, ...assignedCards} = cardsList ?? {};
+ const cardListTyped: Record | undefined = (cardsList as {cardList?: Record})?.cardList ?? {};
+ const assignedCards = Object.fromEntries(Object.entries(cardsList ?? {}).filter(([key]) => key !== 'cardList')) as Record;
const [cardFeeds] = useCardFeeds(policyID);
const companyFeeds = getCompanyFeeds(cardFeeds);
@@ -68,15 +75,38 @@ function WorkspaceCompanyCardsTable({selectedFeed, cardsList, policyID, onAssign
const assignedCard = Object.values(assignedCards ?? {}).find(assignedCardPredicate);
- const cardholder = personalDetails?.[assignedCard?.accountID ?? CONST.DEFAULT_NUMBER_ID];
+ const cardholder = assignedCard?.accountID ? personalDetails?.[assignedCard.accountID] : undefined;
- const customCardName = customCardNames?.[assignedCard?.cardID ?? CONST.DEFAULT_NUMBER_ID] ?? getCardDefaultName(cardholder?.displayName);
+ const customCardName = assignedCard?.cardID ? customCardNames?.[assignedCard.cardID] : undefined;
const isCardDeleted = assignedCard?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const isAssigned = !!assignedCard;
- return {cardName, customCardName, isCardDeleted, isAssigned, assignedCard, cardholder};
+ // Calculate cardIdentifier for unassigned cards
+ let cardIdentifier: string | undefined;
+ if (!assignedCard) {
+ const isPlaid = !!getPlaidInstitutionId(selectedFeed);
+ const isCommercial = isCustomFeed(selectedFeed);
+
+ if (isPlaid) {
+ cardIdentifier = cardName;
+ } else if (isCommercial) {
+ const cardValue = cardListTyped?.[cardName] ?? cardName;
+ const digitsOnly = cardValue.replaceAll(/\D/g, '');
+ if (digitsOnly.length >= 10) {
+ const first6 = digitsOnly.substring(0, 6);
+ const last4 = digitsOnly.substring(digitsOnly.length - 4);
+ cardIdentifier = `${first6}${last4}`;
+ } else {
+ cardIdentifier = cardValue;
+ }
+ } else {
+ cardIdentifier = cardListTyped?.[cardName] ?? cardName;
+ }
+ }
+
+ return {cardName, customCardName, isCardDeleted, isAssigned, assignedCard, cardholder, cardIdentifier};
}) ?? [];
const renderItem = ({item, index}: ListRenderItemInfo) => (
@@ -87,7 +117,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, cardsList, policyID, onAssign
selectedFeed={selectedFeed}
plaidIconUrl={getPlaidInstitutionIconUrl(selectedFeed)}
isPlaidCardFeed={isPlaidCardFeed}
- onAssignCard={onAssignCard}
+ onAssignCard={() => onAssignCard(item.cardIdentifier)}
isAssigningCardDisabled={isAssigningCardDisabled}
shouldUseNarrowTableRowLayout={shouldShowNarrowLayout}
/>
@@ -222,7 +252,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, cardsList, policyID, onAssign
}, [activeSortingInWideLayout, shouldShowNarrowLayout]);
// Show empty state when there are no cards
- if (!data.length) {
+ if (!data.length && !isLoadingCardsTableData) {
return (
onAssignCard()}
isAssigningCardDisabled={isAssigningCardDisabled}
/>
@@ -241,7 +271,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, cardsList, policyID, onAssign
return (
: undefined}
>
Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed)));
+ Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed})));
return;
}
setAssignCardStepAndData({data, currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION});
- Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute(policyID, selectedFeed)));
+ Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed})));
};
const secondaryActions = [
diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx
index b20d0d816acc..384182b87d1f 100644
--- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx
+++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx
@@ -40,6 +40,9 @@ type WorkspaceCompanyCardTableItemData = {
/** Whether the card is assigned */
isAssigned: boolean;
+
+ /** Card identifier for unassigned cards */
+ cardIdentifier?: string;
};
type WorkspaceCompanyCardTableItemProps = {
diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx
index 938b798f725a..0bffd8283237 100644
--- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx
+++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx
@@ -4,13 +4,14 @@ import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import ScreenWrapper from '@components/ScreenWrapper';
import useInitial from '@hooks/useInitial';
import useOnyx from '@hooks/useOnyx';
+import {getCompanyCardFeed} from '@libs/CardUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@navigation/types';
import PlaidConnectionStep from '@pages/workspace/companyCards/addNew/PlaidConnectionStep';
import BankConnection from '@pages/workspace/companyCards/BankConnection';
import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading';
import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading';
-import {clearAssignCardStepAndData} from '@userActions/CompanyCards';
+import {clearAssignCardStepAndData, setAssignCardStepAndData} from '@userActions/CompanyCards';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
@@ -25,10 +26,12 @@ import TransactionStartDateStep from './TransactionStartDateStep';
type AssignCardFeedPageProps = PlatformStackScreenProps & WithPolicyAndFullscreenLoadingProps;
function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
+ const feed = decodeURIComponent(route.params?.feed) as CompanyCardFeedWithDomainID;
+ const cardID = route.params?.cardID ? decodeURIComponent(route.params?.cardID) : undefined;
+
const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true});
const currentStep = assignCard?.currentStep;
- const feed = decodeURIComponent(route.params?.feed) as CompanyCardFeedWithDomainID;
const backTo = route.params?.backTo;
const policyID = policy?.id;
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector, canBeMissing: true});
@@ -41,6 +44,21 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
};
}, []);
+ useEffect(() => {
+ if (!cardID || currentStep) {
+ return;
+ }
+ const companyCardFeed = getCompanyCardFeed(feed);
+
+ setAssignCardStepAndData({
+ currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE,
+ data: {
+ bankName: companyCardFeed,
+ encryptedCardNumber: cardID,
+ },
+ });
+ }, [cardID, currentStep, feed]);
+
if (isActingAsDelegate) {
return (
);
@@ -84,7 +101,7 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
/>
);
case CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE:
- return ;
+ return ;
case CONST.COMPANY_CARD.STEP.CARD_NAME:
return ;
case CONST.COMPANY_CARD.STEP.CONFIRMATION:
@@ -106,7 +123,6 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) {
return (
);
diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx
index b435955b4877..7851c9f0f9e5 100644
--- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx
+++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx
@@ -1,15 +1,14 @@
+import {format} from 'date-fns';
import {Str} from 'expensify-common';
import React, {useEffect, useMemo, useState} from 'react';
import {Keyboard} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
-import * as Expensicons from '@components/Icon/Expensicons';
import InteractiveStepWrapper from '@components/InteractiveStepWrapper';
import SelectionList from '@components/SelectionList';
import UserListItem from '@components/SelectionList/ListItem/UserListItem';
import type {ListItem} from '@components/SelectionList/types';
import Text from '@components/Text';
-import useCardFeeds from '@hooks/useCardFeeds';
-import useCardsList from '@hooks/useCardsList';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
@@ -17,7 +16,7 @@ import useSearchSelector from '@hooks/useSearchSelector';
import useThemeStyles from '@hooks/useThemeStyles';
import {setDraftInviteAccountID} from '@libs/actions/Card';
import {searchInServer} from '@libs/actions/Report';
-import {getDefaultCardName, getFilteredCardList, hasOnlyOneCardToAssign} from '@libs/CardUtils';
+import {getDefaultCardName} from '@libs/CardUtils';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import {getHeaderMessage, getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils';
@@ -36,24 +35,18 @@ type AssigneeStepProps = {
/** The policy that the card will be issued under */
policy: OnyxEntry;
- /** Selected feed */
- feed: OnyxTypes.CompanyCardFeedWithDomainID;
-
/** Route params */
route: PlatformStackRouteProp;
};
-function AssigneeStep({policy, feed, route}: AssigneeStepProps) {
+function AssigneeStep({policy, route}: AssigneeStepProps) {
const policyID = route.params.policyID;
const {translate, formatPhoneNumber, localeCompare} = useLocalize();
const styles = useThemeStyles();
const {isOffline} = useNetwork();
+ const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const);
const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true});
- const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: false});
const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false});
- const [list] = useCardsList(feed);
- const [cardFeeds] = useCardFeeds(policyID);
- const filteredCardList = getFilteredCardList(list, cardFeeds?.[feed]?.accountList, workspaceCardFeeds);
const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false, canBeMissing: true});
@@ -89,9 +82,18 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) {
};
Keyboard.dismiss();
+
if (assignee?.login === assignCard?.data?.email) {
+ if (assignCard?.data?.encryptedCardNumber) {
+ nextStep = CONST.COMPANY_CARD.STEP.CONFIRMATION;
+ data.encryptedCardNumber = assignCard.data.encryptedCardNumber;
+ data.cardNumber = assignCard.data.cardNumber;
+ data.startDate = data.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING);
+ data.dateOption = data.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM;
+ }
setAssignCardStepAndData({
currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : nextStep,
+ data,
isEditing: false,
});
return;
@@ -109,10 +111,12 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) {
return;
}
- if (hasOnlyOneCardToAssign(filteredCardList)) {
- nextStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE;
- data.cardNumber = Object.keys(filteredCardList).at(0);
- data.encryptedCardNumber = Object.values(filteredCardList).at(0);
+ if (assignCard?.data?.encryptedCardNumber) {
+ nextStep = CONST.COMPANY_CARD.STEP.CONFIRMATION;
+ data.encryptedCardNumber = assignCard.data.encryptedCardNumber;
+ data.cardNumber = assignCard.data.cardNumber;
+ data.startDate = data.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING);
+ data.dateOption = data.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM;
}
setAssignCardStepAndData({
@@ -154,7 +158,7 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) {
isSelected: assignCard?.data?.email === email,
icons: [
{
- source: personalDetail?.avatar ?? Expensicons.FallbackAvatar,
+ source: personalDetail?.avatar ?? icons.FallbackAvatar,
name: formatPhoneNumber(email),
type: CONST.ICON_TYPE_AVATAR,
id: personalDetail?.accountID,
@@ -166,7 +170,7 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) {
membersList = sortAlphabetically(membersList, 'text', localeCompare);
return membersList;
- }, [isOffline, policy?.employeeList, assignCard?.data?.email, formatPhoneNumber, localeCompare]);
+ }, [isOffline, policy?.employeeList, assignCard?.data?.email, formatPhoneNumber, localeCompare, icons.FallbackAvatar]);
const assignees = useMemo(() => {
if (!debouncedSearchTerm) {
@@ -229,13 +233,11 @@ function AssigneeStep({policy, feed, route}: AssigneeStepProps) {
setDidScreenTransitionEnd(true)}
>
- {translate('workspace.companyCards.whoNeedsCardAssigned')}
+ {translate('workspace.companyCards.chooseTheCardholder')}
{translate('workspace.moreFeatures.companyCards.giveItNameInstruction')}
state?.routes?.findLast((route) => isFullScreenName(route.name)));
@@ -98,17 +103,14 @@ function ConfirmationStep({policyID, feed, backTo}: ConfirmationStepProps) {
};
const handleBackButtonPress = () => {
- setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE});
+ setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE});
};
return (
@@ -119,18 +121,20 @@ function ConfirmationStep({policyID, feed, backTo}: ConfirmationStepProps) {
>
{translate('workspace.companyCards.letsDoubleCheck')}
{translate('workspace.companyCards.confirmationDescription')}
- editStep(CONST.COMPANY_CARD.STEP.ASSIGNEE)}
- />
+