From 4d662af05f861d204a29f46001885c4d4075e2f9 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 18:59:06 -0500 Subject: [PATCH 01/36] refactor: make `onAssignCard` callbcak `cardID` required --- .../workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx index c1c657a8516a8..2bd3510c98fdb 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx @@ -65,7 +65,7 @@ type WorkspaceCompanyCardTableItemProps = { shouldUseNarrowTableRowLayout?: boolean; /** On assign card callback */ - onAssignCard: (cardID?: string) => void; + onAssignCard: (cardID: string) => void; }; function WorkspaceCompanyCardTableItem({ From 819154ab20da8d32bdf9426ee2f50946f61558e0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 18:59:18 -0500 Subject: [PATCH 02/36] fix: update empty table component --- src/languages/en.ts | 4 ++-- ...orkspaceCompanyCardsFeedAddedEmptyPage.tsx | 20 ++----------------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 865ec9235fd91..c6ee5ba8e97e4 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4990,8 +4990,8 @@ const translations = { personal: 'Allow deleting transactions', setFeedNameDescription: 'Give the card feed a unique name so you can tell it apart from the others', setTransactionLiabilityDescription: 'When enabled, cardholders can delete card transactions. New transactions will follow this rule.', - emptyAddedFeedTitle: 'Assign company cards', - emptyAddedFeedDescription: 'Get started by assigning your first card to a member.', + emptyAddedFeedTitle: 'No cards in this feed', + emptyAddedFeedDescription: "Make sure there are cards in your bank's card feed.", pendingFeedTitle: `We're reviewing your request...`, pendingFeedDescription: `We're currently reviewing your feed details. Once that's done, we'll reach out to you via`, pendingBankTitle: 'Check your browser window', diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsFeedAddedEmptyPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsFeedAddedEmptyPage.tsx index 95ed481a589d6..a611b3aa2eafd 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsFeedAddedEmptyPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsFeedAddedEmptyPage.tsx @@ -3,28 +3,21 @@ import EmptyStateComponent from '@components/EmptyStateComponent'; import ScrollView from '@components/ScrollView'; import CardRowSkeleton from '@components/Skeletons/CardRowSkeleton'; import Text from '@components/Text'; -import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import colors from '@styles/theme/colors'; import CONST from '@src/CONST'; type WorkspaceCompanyCardsFeedAddedEmptyPageProps = { - /** Handle assign card action */ - handleAssignCard: () => void; - - /** Whether to disable assign card button */ - isAssigningCardDisabled?: boolean; - /** Whether to disable GB disclaimer */ shouldShowGBDisclaimer?: boolean; }; -function WorkspaceCompanyCardsFeedAddedEmptyPage({handleAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer}: WorkspaceCompanyCardsFeedAddedEmptyPageProps) { +function WorkspaceCompanyCardsFeedAddedEmptyPage({shouldShowGBDisclaimer}: WorkspaceCompanyCardsFeedAddedEmptyPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['CompanyCardsEmptyState']); - const Expensicons = useMemoizedLazyExpensifyIcons(['Plus']); return ( {!!shouldShowGBDisclaimer && {translate('workspace.companyCards.ukRegulation')}} From b9c3a0602017c55c57cb8aa49cc242daa5bfe6ac Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 19:00:18 -0500 Subject: [PATCH 03/36] fix: remove unused props --- .../workspace/companyCards/WorkspaceCompanyCardsTable.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx index 5523275d62ae1..5b7519452d56d 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx @@ -234,11 +234,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAss policyID={policyID} selectedFeed={selectedFeed} /> - onAssignCard()} - isAssigningCardDisabled={isAssigningCardDisabled} - /> + ); } From 03170b4b6ba872c3067548a9c432313ff08d8ba2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 19:01:00 -0500 Subject: [PATCH 04/36] fix: onAssignCard param type --- src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx index 5b7519452d56d..72938d1ecefb0 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx @@ -31,7 +31,7 @@ type WorkspaceCompanyCardsTableProps = { policyID: string; /** On assign card callback */ - onAssignCard: (cardID?: string) => void; + onAssignCard: (cardID: string) => void; /** Whether to disable assign card button */ isAssigningCardDisabled?: boolean; From e7f419812007229b68d272963daa996c72ba752b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 19:04:38 -0500 Subject: [PATCH 05/36] fix: remove header subtitle --- .../companyCards/assignCard/TransactionStartDateStep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx index 453a50c2bde72..3c0cd0f99e244 100644 --- a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx @@ -98,7 +98,6 @@ function TransactionStartDateStep({route}: {route: PlatformStackRouteProp {translate('workspace.companyCards.startDateDescription')} From c27127f4801caf3f5f30c5a1c0cbfae0e8e88493 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 21:15:58 -0500 Subject: [PATCH 06/36] fix: URI encode feed name --- src/ROUTES.ts | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 905e0a4d9ba38..e3d62b6096d9b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -16,14 +16,15 @@ import type {ReimbursementAccountStepToOpen} from './libs/ReimbursementAccountUt import {getUrlWithParams} from './libs/Url'; import SCREENS from './SCREENS'; import type {Screen} from './SCREENS'; +import type {CompanyCardFeedWithDomainID} from './types/onyx'; 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; + feed: CompanyCardFeedWithDomainID; + cardID?: string; }; // This is a file containing constants for all the routes we want to be able to go to @@ -2167,14 +2168,14 @@ const ROUTES = { }, }, WORKSPACE_COMPANY_CARDS_BANK_CONNECTION: { - route: 'workspaces/:policyID/company-cards/:bankName/bank-connection', - getRoute: (policyID: string | undefined, bankName: string, backTo: string) => { + route: 'workspaces/:policyID/company-cards/:feed/bank-connection', + getRoute: (policyID: string | undefined, feed: string, backTo: string) => { if (!policyID) { Log.warn('Invalid policyID is used to build the WORKSPACE_COMPANY_CARDS_BANK_CONNECTION route'); } // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - return getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${bankName}/bank-connection`, backTo); + return getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${feed}/bank-connection`, backTo); }, }, WORKSPACE_COMPANY_CARDS_ADD_NEW: { @@ -2188,17 +2189,17 @@ 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/:cardID', - - getRoute: (params: WorkspaceCompanyCardsAssignCardParams, backTo?: string) => + route: 'workspaces/:policyID/company-cards/:feed/assign-card', + getRoute: ({feed, cardID, policyID}: 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}`, backTo), + getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/assign-card${cardID ? `?cardID=${cardID}` : ''}`, backTo), }, WORKSPACE_COMPANY_CARD_DETAILS: { route: 'workspaces/:policyID/company-cards/:bank/:cardID', - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getRoute: (policyID: string, cardID: string, bank: string, backTo?: string) => getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${bank}/${cardID}`, backTo), + getRoute: (policyID: string, cardID: string, bank: string, backTo?: string) => + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(bank)}/${encodeURIComponent(cardID)}`, backTo), }, WORKSPACE_COMPANY_CARD_NAME: { route: 'workspaces/:policyID/company-cards/:bank/:cardID/edit/name', From e1fb42bb01629e923b50c31ead05b253d775efae Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 21:16:17 -0500 Subject: [PATCH 07/36] update types --- src/libs/Navigation/types.ts | 4 ++-- src/types/onyx/index.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index aaef371260fbd..c90df56829a5b 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -24,7 +24,7 @@ import type {Country, IOUAction, IOUType} from '@src/CONST'; import type NAVIGATORS from '@src/NAVIGATORS'; import type {Route as ExpensifyRoute, Route as Routes} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeed} from '@src/types/onyx'; +import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {ConnectionName, SageIntacctMappingName} from '@src/types/onyx/Policy'; import type {CustomFieldType} from '@src/types/onyx/PolicyEmployee'; import type {FileObject} from '@src/types/utils/Attachment'; @@ -1201,7 +1201,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; 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 diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 2dab239af2f68..f3d6364aa921c 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -4,7 +4,7 @@ import type Account from './Account'; import type AccountData from './AccountData'; import type AppReview from './AppReview'; import type {ApprovalWorkflowOnyx} from './ApprovalWorkflow'; -import type {AssignCard} from './AssignCard'; +import type {AssignCard, AssignCardData} from './AssignCard'; import type {BankAccountList} from './BankAccount'; import type BankAccount from './BankAccount'; import type Beta from './Beta'; @@ -172,6 +172,7 @@ export type { GpsDraftDetails, IntroSelected, IssueNewCard, + AssignCardData, AddNewCompanyCardFeed, CompanyCardFeed, CardContinuousReconciliation, From 633989959c2cef1d7c5df695be212ee37d30a2b5 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 21:16:33 -0500 Subject: [PATCH 08/36] refactor: remove unused variable --- .../companyCards/assignCard/TransactionStartDateStep.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx index 3c0cd0f99e244..b3db110bab74b 100644 --- a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx @@ -28,7 +28,6 @@ function TransactionStartDateStep({route}: {route: PlatformStackRouteProp Date: Thu, 18 Dec 2025 21:29:08 -0500 Subject: [PATCH 09/36] fix: simplify navigating to card assign flow --- src/hooks/useAssignCard.ts | 83 +---------- ...orkspaceCompanyCardsTableHeaderButtons.tsx | 33 +---- .../assignCard/AssignCardFeedPage.tsx | 140 ++++++++++++++---- 3 files changed, 117 insertions(+), 139 deletions(-) diff --git a/src/hooks/useAssignCard.ts b/src/hooks/useAssignCard.ts index 13e7fc28bf475..8f055443f7569 100644 --- a/src/hooks/useAssignCard.ts +++ b/src/hooks/useAssignCard.ts @@ -1,32 +1,14 @@ import {useContext} from 'react'; import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider'; -import {importPlaidAccounts} from '@libs/actions/Plaid'; -import { - checkIfFeedConnectionIsBroken, - filterInactiveCards, - getCompanyCardFeed, - getCompanyFeeds, - getDomainOrWorkspaceAccountID, - getFilteredCardList, - getPlaidCountry, - getPlaidInstitutionId, - hasOnlyOneCardToAssign, - isCustomFeed, - isSelectedFeedExpired, -} from '@libs/CardUtils'; +import {checkIfFeedConnectionIsBroken, filterInactiveCards, getCompanyFeeds, getDomainOrWorkspaceAccountID, isCustomFeed} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; -import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; -import {getDomainNameForPolicy, isDeletedPolicyEmployee} from '@libs/PolicyUtils'; -import {clearAddNewCardFlow, openPolicyCompanyCardsPage, setAddNewCompanyCardStepAndData, setAssignCardStepAndData} from '@userActions/CompanyCards'; +import {clearAddNewCardFlow, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {CompanyCardFeedWithDomainID, CurrencyList} from '@src/types/onyx'; -import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; -import {getEmptyObject} from '@src/types/utils/EmptyObject'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {CombinedCardFeed} from './useCardFeeds'; import useCardFeeds from './useCardFeeds'; -import useCardsList from './useCardsList'; import useIsAllowedToIssueCompanyCard from './useIsAllowedToIssueCompanyCard'; import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; @@ -45,15 +27,6 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false}); const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; - const [workspaceCardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: true}); - const feed = selectedFeed ? getCompanyCardFeed(selectedFeed) : undefined; - - const [cardsList] = useCardsList(selectedFeed); - - const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); - const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); - - const filteredCardList = getFilteredCardList(cardsList, selectedFeed ? cardFeeds?.[selectedFeed]?.accountList : undefined, workspaceCardFeeds); const companyCards = getCompanyFeeds(cardFeeds); const selectedFeedData = selectedFeed && companyCards[selectedFeed]; @@ -74,7 +47,7 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA const isAssigningCardDisabled = !currentFeedData || !!currentFeedData?.pending || isSelectedFeedConnectionBroken || !isAllowedToIssueCompanyCard; - const assignCard = (cardID: string) => { + const assignCard = (cardID?: string) => { if (isAssigningCardDisabled) { return; } @@ -97,55 +70,7 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA return; } - const data: Partial = { - 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); - const plaidAccessToken = selectedFeedData?.plaidAccessToken; - - // Refetch plaid card list - if (!isFeedExpired && plaidAccessToken) { - const country = selectedFeedData?.country ?? ''; - importPlaidAccounts('', selectedFeed, '', country, getDomainNameForPolicy(policyID), '', undefined, undefined, plaidAccessToken); - } - - if (!cardID && employeeList.length === 1) { - const userEmail = Object.keys(policy?.employeeList ?? {}).at(0) ?? ''; - data.email = userEmail; - const personalDetails = getPersonalDetailByEmail(userEmail); - const memberName = personalDetails?.firstName ? personalDetails.firstName : personalDetails?.login; - data.cardName = `${memberName}'s card`; - currentStep = CONST.COMPANY_CARD.STEP.CARD; - - if (hasOnlyOneCardToAssign(filteredCardList)) { - currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; - data.cardNumber = Object.keys(filteredCardList).at(0); - data.encryptedCardNumber = Object.values(filteredCardList).at(0); - } - } - - if (isFeedExpired) { - const institutionId = !!getPlaidInstitutionId(selectedFeed); - if (institutionId) { - const country = getPlaidCountry(policy?.outputCurrency, currencyList, countryByIp); - setAddNewCompanyCardStepAndData({ - data: { - selectedCountry: country, - }, - }); - } - currentStep = institutionId ? CONST.COMPANY_CARD.STEP.PLAID_CONNECTION : CONST.COMPANY_CARD.STEP.BANK_CONNECTION; - } - clearAddNewCardFlow(); - setAssignCardStepAndData({data, currentStep}); Navigation.setNavigationActionToMicrotaskQueue(() => { Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed, cardID})); }); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx index 7f248df55465e..e1c869ff540f8 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx @@ -9,7 +9,6 @@ import * as Expensicons from '@components/Icon/Expensicons'; import RenderHTML from '@components/RenderHTML'; import Table from '@components/Table'; import Text from '@components/Text'; -import type {CompanyCardFeedWithDomainID} from '@hooks/useCardFeeds'; import useCardFeeds from '@hooks/useCardFeeds'; import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -31,19 +30,14 @@ import { getCompanyFeeds, getCustomOrFormattedFeedName, getDomainOrWorkspaceAccountID, - getPlaidCountry, getPlaidInstitutionIconUrl, - getPlaidInstitutionId, isCustomFeed, } from '@libs/CardUtils'; import Navigation from '@navigation/Navigation'; -import {setAddNewCompanyCardStepAndData, setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {CurrencyList} from '@src/types/onyx'; -import type {AssignCardData} from '@src/types/onyx/AssignCard'; -import {getEmptyObject} from '@src/types/utils/EmptyObject'; +import type {CompanyCardFeedWithDomainID, CurrencyList} from '@src/types/onyx'; type WorkspaceCompanyCardsTableHeaderButtonsProps = { /** Current policy id */ @@ -69,8 +63,6 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, should const [cardFeeds] = useCardFeeds(policyID); const policy = usePolicy(policyID); const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: false}); - const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); - const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); const feed = getCompanyCardFeed(selectedFeed); const formattedFeedName = getCustomOrFormattedFeedName(feed, cardFeeds?.[selectedFeed]?.customFeedName); const isCommercialFeed = isCustomFeed(selectedFeed); @@ -84,29 +76,8 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, should const isSelectedFeedConnectionBroken = checkIfFeedConnectionIsBroken(filteredFeedCards) || hasFeedError; const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${currentFeedData?.domainID}`, {canBeMissing: true}); - const openBankConnection = () => { - const institutionId = !!getPlaidInstitutionId(selectedFeed); - const data: Partial = { - bankName: feed, - }; - if (institutionId) { - const country = getPlaidCountry(policy?.outputCurrency, currencyList, countryByIp); - setAddNewCompanyCardStepAndData({ - data: { - selectedCountry: country, - }, - }); - setAssignCardStepAndData({ - data, - currentStep: CONST.COMPANY_CARD.STEP.PLAID_CONNECTION, - }); - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed}))); - return; - } - - setAssignCardStepAndData({data, currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); + const openBankConnection = () => Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed}))); - }; const secondaryActions = [ { diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx index 98a5052636533..212420e7bef67 100644 --- a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -1,21 +1,31 @@ import {isActingAsDelegateSelector} from '@selectors/Account'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import ScreenWrapper from '@components/ScreenWrapper'; +import useCardFeeds from '@hooks/useCardFeeds'; import useInitial from '@hooks/useInitial'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; -import {getCompanyCardFeed} from '@libs/CardUtils'; +import usePolicy from '@hooks/usePolicy'; +import {importPlaidAccounts} from '@libs/actions/Plaid'; +import {getCompanyCardFeed, getCompanyFeeds, getPlaidCountry, getPlaidInstitutionId, isSelectedFeedExpired} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; +import {getDomainNameForPolicy, isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; +import LoadingPage from '@pages/LoadingPage'; 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, setAssignCardStepAndData} from '@userActions/CompanyCards'; +import {clearAssignCardStepAndData, setAddNewCompanyCardStepAndData, setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import type {CompanyCardFeedWithDomainID, CurrencyList} from '@src/types/onyx'; +import type {AssignCardData} from '@src/types/onyx/AssignCard'; +import {getEmptyObject} from '@src/types/utils/EmptyObject'; import AssigneeStep from './AssigneeStep'; import CardNameStep from './CardNameStep'; import CardSelectionStep from './CardSelectionStep'; @@ -26,38 +36,40 @@ import TransactionStartDateStep from './TransactionStartDateStep'; type AssignCardFeedPageProps = PlatformStackScreenProps & WithPolicyAndFullscreenLoadingProps; function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { + const backTo = route.params?.backTo; const feed = decodeURIComponent(route.params?.feed) as CompanyCardFeedWithDomainID; const cardID = route.params?.cardID ? decodeURIComponent(route.params?.cardID) : undefined; + const policyID = policy?.id; - const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); - const currentStep = assignCard?.currentStep; + const {translate} = useLocalize(); - const backTo = route.params?.backTo; - const policyID = policy?.id; const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector, canBeMissing: true}); + + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); + const currentStep = assignCard?.currentStep; const firstAssigneeEmail = useInitial(assignCard?.data?.email); const shouldUseBackToParam = !firstAssigneeEmail || firstAssigneeEmail === assignCard?.data?.email; + // Set the initial step and data if not already set + const {initialStep, initialStepData} = useInitialAssignCardStep({policyID, feed, cardID}); + const initialStepSet = useRef(false); useEffect(() => { - return () => { - clearAssignCardStepAndData(); - }; - }, []); - - useEffect(() => { - if (!cardID || currentStep) { + if (currentStep || initialStepSet.current) { return; } - const companyCardFeed = getCompanyCardFeed(feed); + initialStepSet.current = true; setAssignCardStepAndData({ - currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, - data: { - bankName: companyCardFeed, - encryptedCardNumber: cardID, - }, + currentStep: initialStep, + data: initialStepData, }); - }, [cardID, currentStep, feed]); + }, [currentStep, initialStep, initialStepData]); + + useEffect(() => { + return () => { + clearAssignCardStepAndData(); + }; + }, []); if (isActingAsDelegate) { return ( @@ -89,7 +101,6 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { case CONST.COMPANY_CARD.STEP.ASSIGNEE: return ( @@ -121,13 +132,84 @@ function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { /> ); default: - return ( - - ); + return ; + } +} + +type UseInitialAssignCardStepProps = { + policyID: string | undefined; + feed: CompanyCardFeedWithDomainID; + cardID: string | undefined; +}; + +function useInitialAssignCardStep({policyID, feed, cardID}: UseInitialAssignCardStepProps) { + const {isOffline} = useNetwork(); + + const policy = usePolicy(policyID); + + const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); + const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); + + const [cardFeeds] = useCardFeeds(policyID); + const companyCards = getCompanyFeeds(cardFeeds); + const feedData = feed && companyCards[feed]; + const bankName = getCompanyCardFeed(feed); + + const data: Partial = { + bankName, + cardNumber: cardID, + encryptedCardNumber: cardID, + }; + + const isFeedExpired = isSelectedFeedExpired(feedData); + const plaidAccessToken = feedData?.plaidAccessToken; + + // Refetch plaid card list + if (!isFeedExpired && plaidAccessToken) { + const country = feedData?.country ?? ''; + importPlaidAccounts('', feed, '', country, getDomainNameForPolicy(policyID), '', undefined, undefined, plaidAccessToken); + } + + if (isFeedExpired || !cardID) { + const institutionId = !!getPlaidInstitutionId(feed); + if (institutionId) { + const country = getPlaidCountry(policy?.outputCurrency, currencyList, countryByIp); + setAddNewCompanyCardStepAndData({ + data: { + selectedCountry: country, + }, + }); + + return { + initialStep: CONST.COMPANY_CARD.STEP.PLAID_CONNECTION, + initialStepData: data, + }; + } + + return { + initialStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION, + initialStepData: data, + }; + } + + const employeeList = Object.values(policy?.employeeList ?? {}).filter((employee) => !isDeletedPolicyEmployee(employee, isOffline)); + if (employeeList.length === 1) { + const userEmail = Object.keys(policy?.employeeList ?? {}).at(0) ?? ''; + data.email = userEmail; + const personalDetails = getPersonalDetailByEmail(userEmail); + const memberName = personalDetails?.firstName ? personalDetails.firstName : personalDetails?.login; + data.cardName = `${memberName}'s card`; + + return { + initialStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, + initialStepData: data, + }; } + + return { + initialStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, + initialStepData: data, + }; } export default withPolicyAndFullscreenLoading(AssignCardFeedPage); From b05a6fc5c98b5ba4838997fdfa9d81f66b93b547 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 23:10:41 -0500 Subject: [PATCH 10/36] feat: extract `CardFeedIcon` component --- src/components/CardFeedIcon.tsx | 67 +++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 src/components/CardFeedIcon.tsx diff --git a/src/components/CardFeedIcon.tsx b/src/components/CardFeedIcon.tsx new file mode 100644 index 0000000000000..cd1b9390294eb --- /dev/null +++ b/src/components/CardFeedIcon.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useThemeIllustrations from '@hooks/useThemeIllustrations'; +import {getCardFeedIcon, getPlaidInstitutionIconUrl, getPlaidInstitutionId} from '@libs/CardUtils'; +import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import type {IconProps} from './Icon'; +import Icon from './Icon'; +import PlaidCardFeedIcon from './PlaidCardFeedIcon'; + +type CardFeedIconProps = { + isExpensifyCardFeed?: boolean; + selectedFeed?: CompanyCardFeedWithDomainID | undefined; + iconProps?: Partial; +}; + +function CardFeedIcon({iconProps, selectedFeed, isExpensifyCardFeed = false}: CardFeedIconProps) { + const {src, ...restIconProps} = iconProps ?? {}; + + const illustrations = useThemeIllustrations(); + const companyCardFeedIcons = useCompanyCardFeedIcons(); + + const isPlaidCardFeed = !!getPlaidInstitutionId(selectedFeed); + + if (isExpensifyCardFeed) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; + } + + if (isPlaidCardFeed) { + return ( + + ); + } + + if (!selectedFeed) { + return null; + } + + return ( + + ); +} + +function ExpensifyCardFeedIcon(iconProps: Partial) { + const {src, ...restIconProps} = iconProps ?? {}; + + const memoizedIllustrations = useMemoizedLazyIllustrations(['ExpensifyCardImage']); + + return ( + + ); +} + +export default CardFeedIcon; From 26fbc4441ed5d9aa89ed0414b9db92dae154d179 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Thu, 18 Dec 2025 23:11:01 -0500 Subject: [PATCH 11/36] fix: lift cardFeedIcon up in React view hierarchy --- src/components/FeedSelector.tsx | 27 ++++---------- src/components/Icon/index.tsx | 1 + .../WorkspaceCompanyCardsPage.tsx | 23 ++++++++++-- .../WorkspaceCompanyCardsTable.tsx | 12 ++++-- ...orkspaceCompanyCardsTableHeaderButtons.tsx | 25 ++++++------- .../WorkspaceCompanyCardsTableItem.tsx | 37 +++---------------- .../WorkspaceExpensifyCardListPage.tsx | 3 +- 7 files changed, 55 insertions(+), 73 deletions(-) diff --git a/src/components/FeedSelector.tsx b/src/components/FeedSelector.tsx index 1fcf129582bee..505dbd02b9ec1 100644 --- a/src/components/FeedSelector.tsx +++ b/src/components/FeedSelector.tsx @@ -3,11 +3,8 @@ import {View} from 'react-native'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; -import type IconAsset from '@src/types/utils/IconAsset'; import CaretWrapper from './CaretWrapper'; import Icon from './Icon'; -import PlaidCardFeedIcon from './PlaidCardFeedIcon'; import {PressableWithFeedback} from './Pressable'; import Text from './Text'; @@ -16,7 +13,7 @@ type Props = { onFeedSelect: () => void; /** Icon for the card */ - cardIcon: IconAsset; + CardFeedIcon: React.ReactNode; /** Feed name */ feedName?: string; @@ -26,15 +23,13 @@ type Props = { /** Whether the RBR indicator should be shown */ shouldShowRBR?: boolean; - - /** Image url for plaid bank account */ - plaidUrl?: string | null; }; -function FeedSelector({onFeedSelect, cardIcon, feedName, supportingText, shouldShowRBR = false, plaidUrl = null}: Props) { +function FeedSelector({onFeedSelect, CardFeedIcon, feedName, supportingText, shouldShowRBR = false}: Props) { const styles = useThemeStyles(); const theme = useTheme(); - const Expensicons = useMemoizedLazyExpensifyIcons(['DotIndicator'] as const); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator'] as const); + return ( - {plaidUrl ? ( - - ) : ( - - )} + {CardFeedIcon} + @@ -64,7 +51,7 @@ function FeedSelector({onFeedSelect, cardIcon, feedName, supportingText, shouldS {shouldShowRBR && ( )} diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index e4038ee9fd11a..51d398a3e85db 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -188,3 +188,4 @@ function Icon({ } export default Icon; +export type {IconProps}; diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 8705c25820d34..b530dc553ef4d 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -1,5 +1,6 @@ import React, {useEffect, useState} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; +import CardFeedIcon from '@components/CardFeedIcon'; import DecisionModal from '@components/DecisionModal'; import useAssignCard from '@hooks/useAssignCard'; import useCardFeeds from '@hooks/useCardFeeds'; @@ -16,6 +17,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import variables from '@styles/variables'; import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -33,7 +35,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const illustrations = useMemoizedLazyIllustrations(['CompanyCard']); + const memoizedIllustrations = useMemoizedLazyIllustrations(['CompanyCard']); const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isBetaEnabled} = usePermissions(); @@ -41,6 +43,7 @@ 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] = useCardFeeds(policyID); const selectedFeed = getSelectedFeed(lastSelectedFeed, cardFeeds); const companyFeeds = getCompanyFeeds(cardFeeds); @@ -53,10 +56,22 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { const isFeedAdded = !isFeedPending && !isNoFeed; const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeedData); - const {isOffline} = useNetwork({ onReconnect: () => openPolicyCompanyCardsPage(policyID, domainOrWorkspaceAccountID), }); + + const cardFeedIcon = ( + + ); + const isLoading = !isOffline && (!cardFeeds || (isFeedAdded && isLoadingOnyxValue(cardsListMetadata))); const isGB = countryByIp === CONST.COUNTRY.GB; const shouldShowGBDisclaimer = isGB && isBetaEnabled(CONST.BETAS.PLAID_COMPANY_CARDS) && (isNoFeed || hasNoAssignedCard); @@ -89,7 +104,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { {!isLoading && ( )} @@ -119,6 +135,7 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { policyID={policyID} onAssignCard={assignCard} isAssigningCardDisabled={isAssigningCardDisabled} + CardFeedIcon={cardFeedIcon} /> )} diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx index 72938d1ecefb0..14e8bbf29df32 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx @@ -11,7 +11,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCompanyFeeds, getPlaidInstitutionIconUrl, getPlaidInstitutionId, isMaskedCardNumberEqual} from '@libs/CardUtils'; +import {getCompanyFeeds, getPlaidInstitutionId, isMaskedCardNumberEqual} from '@libs/CardUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card, CompanyCardFeedWithDomainID} from '@src/types/onyx'; @@ -38,9 +38,12 @@ type WorkspaceCompanyCardsTableProps = { /** Whether to show GB disclaimer */ shouldShowGBDisclaimer?: boolean; + + /** Card feed icon */ + CardFeedIcon?: React.ReactNode; }; -function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer}: WorkspaceCompanyCardsTableProps) { +function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer, CardFeedIcon}: WorkspaceCompanyCardsTableProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate, localeCompare} = useLocalize(); @@ -89,8 +92,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAss key={`${item.cardName}_${index}`} item={item} policyID={policyID} - selectedFeed={selectedFeed} - plaidIconUrl={getPlaidInstitutionIconUrl(selectedFeed)} + CardFeedIcon={CardFeedIcon} isPlaidCardFeed={isPlaidCardFeed} onAssignCard={onAssignCard} isAssigningCardDisabled={isAssigningCardDisabled} @@ -233,6 +235,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAss @@ -257,6 +260,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAss policyID={policyID} selectedFeed={selectedFeed} shouldDisplayTableComponents + CardFeedIcon={CardFeedIcon} /> diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx index e1c869ff540f8..4b7cf08aecfe3 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx @@ -5,19 +5,16 @@ import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import FeedSelector from '@components/FeedSelector'; import Icon from '@components/Icon'; // eslint-disable-next-line no-restricted-imports -import * as Expensicons from '@components/Icon/Expensicons'; import RenderHTML from '@components/RenderHTML'; import Table from '@components/Table'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; -import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; -import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; import { @@ -25,19 +22,18 @@ import { filterInactiveCards, flatAllCardsList, getBankName, - getCardFeedIcon, getCompanyCardFeed, getCompanyFeeds, getCustomOrFormattedFeedName, getDomainOrWorkspaceAccountID, - getPlaidInstitutionIconUrl, + getPlaidInstitutionId, isCustomFeed, } from '@libs/CardUtils'; import Navigation from '@navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {CompanyCardFeedWithDomainID, CurrencyList} from '@src/types/onyx'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; type WorkspaceCompanyCardsTableHeaderButtonsProps = { /** Current policy id */ @@ -48,17 +44,19 @@ type WorkspaceCompanyCardsTableHeaderButtonsProps = { /** Whether the feed is pending */ shouldDisplayTableComponents?: boolean; + + /** Card feed icon */ + CardFeedIcon?: React.ReactNode; }; -function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, shouldDisplayTableComponents = false}: WorkspaceCompanyCardsTableHeaderButtonsProps) { +function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, shouldDisplayTableComponents = false, CardFeedIcon}: WorkspaceCompanyCardsTableHeaderButtonsProps) { const styles = useThemeStyles(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const {translate} = useLocalize(); const theme = useTheme(); - const illustrations = useThemeIllustrations(); const icons = useMemoizedLazyExpensifyIcons(['Gear']); + const expensifyIcons = useMemoizedLazyExpensifyIcons(['DotIndicator']); - const companyCardFeedIcons = useCompanyCardFeedIcons(); const workspaceAccountID = useWorkspaceAccountID(policyID); const [cardFeeds] = useCardFeeds(policyID); const policy = usePolicy(policyID); @@ -66,10 +64,10 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, should const feed = getCompanyCardFeed(selectedFeed); const formattedFeedName = getCustomOrFormattedFeedName(feed, cardFeeds?.[selectedFeed]?.customFeedName); const isCommercialFeed = isCustomFeed(selectedFeed); - const plaidUrl = getPlaidInstitutionIconUrl(selectedFeed); + const isPlaidCardFeed = !!getPlaidInstitutionId(selectedFeed); const companyFeeds = getCompanyFeeds(cardFeeds); const currentFeedData = companyFeeds?.[selectedFeed]; - const bankName = plaidUrl && formattedFeedName ? formattedFeedName : getBankName(feed); + const bankName = isPlaidCardFeed && formattedFeedName ? formattedFeedName : getBankName(feed); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, currentFeedData); const filteredFeedCards = filterInactiveCards(allFeedsCards?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${domainOrWorkspaceAccountID}_${selectedFeed}`]); const hasFeedError = !!cardFeeds?.[selectedFeed]?.errors; @@ -107,9 +105,8 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, should ]} > Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))} - cardIcon={getCardFeedIcon(feed, illustrations, companyCardFeedIcons)} + CardFeedIcon={CardFeedIcon} feedName={formattedFeedName} supportingText={supportingText} shouldShowRBR={checkIfFeedConnectionIsBroken(flatAllCardsList(allFeedsCards, domainOrWorkspaceAccountID), selectedFeed)} @@ -135,7 +132,7 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, should {isSelectedFeedConnectionBroken && !!bankName && ( diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx index 2bd3510c98fdb..5cb5f4d756507 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableItem.tsx @@ -4,23 +4,19 @@ import Avatar from '@components/Avatar'; import Button from '@components/Button'; import Icon from '@components/Icon'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import PlaidCardFeedIcon from '@components/PlaidCardFeedIcon'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; import TextWithTooltip from '@components/TextWithTooltip'; -import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; -import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCardFeedIcon, getCompanyCardFeedWithDomainID, lastFourNumbersFromCardName, splitMaskedCardNumber} from '@libs/CardUtils'; +import {getCompanyCardFeedWithDomainID, lastFourNumbersFromCardName, splitMaskedCardNumber} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getDefaultAvatarURL} from '@libs/UserAvatarUtils'; -import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Card, CompanyCardFeed, CompanyCardFeedWithDomainID, PersonalDetails} from '@src/types/onyx'; +import type {Card, CompanyCardFeed, PersonalDetails} from '@src/types/onyx'; type WorkspaceCompanyCardTableItemData = { /** Card number */ @@ -49,11 +45,8 @@ type WorkspaceCompanyCardTableItemProps = { /** Policy ID */ policyID: string; - /** Selected feed */ - selectedFeed: CompanyCardFeedWithDomainID; - - /** Plaid URL */ - plaidIconUrl?: string; + /** Card feed icon element */ + CardFeedIcon?: React.ReactNode; /** Whether to disable assign card button */ isAssigningCardDisabled?: boolean; @@ -71,8 +64,7 @@ type WorkspaceCompanyCardTableItemProps = { function WorkspaceCompanyCardTableItem({ item: {cardName, customCardName, assignedCard, isAssigned, cardholder, isCardDeleted}, policyID, - selectedFeed, - plaidIconUrl, + CardFeedIcon, isPlaidCardFeed, shouldUseNarrowTableRowLayout, isAssigningCardDisabled, @@ -81,15 +73,8 @@ function WorkspaceCompanyCardTableItem({ const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); - const illustrations = useThemeIllustrations(); - const companyCardFeedIcons = useCompanyCardFeedIcons(); const Expensicons = useMemoizedLazyExpensifyIcons(['ArrowRight']); - let cardFeedIcon = null; - if (!plaidIconUrl) { - cardFeedIcon = getCardFeedIcon(selectedFeed as CompanyCardFeed, illustrations, companyCardFeedIcons); - } - const lastCardNumbers = isPlaidCardFeed ? lastFourNumbersFromCardName(cardName) : splitMaskedCardNumber(cardName)?.lastDigits; const alternateLoginText = shouldUseNarrowTableRowLayout ? `${customCardName}${lastCardNumbers ? ` - ${lastCardNumbers}` : ''}` : (cardholder?.login ?? ''); @@ -157,17 +142,7 @@ function WorkspaceCompanyCardTableItem({ ) : ( <> - {!!plaidIconUrl && } - - {!plaidIconUrl && !!cardFeedIcon && ( - - )} - + {CardFeedIcon} Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SELECT_FEED.getRoute(policyID))} - cardIcon={illustrations.ExpensifyCardImage} + CardFeedIcon={} feedName={translate('workspace.common.expensifyCard')} supportingText={getDescriptionForPolicyDomainCard(cardSettings?.domainName ?? '')} /> From 0d338e78df205e666cf2c6f792b28af7bd3d8518 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 09:54:27 -0500 Subject: [PATCH 12/36] fix: apply `finallyData` if "simulate failing network request flag is on" --- src/libs/Network/SequentialQueue.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 5c8b6fb0e6e70..a3707e431ec0a 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -170,7 +170,7 @@ function process(): Promise { // Duplicate records don't need to be retried as they just mean the record already exists on the server if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD || shouldFailAllRequests) { if (shouldFailAllRequests) { - Onyx.update(requestToProcess.failureData ?? []); + Onyx.update([...(requestToProcess.failureData ?? []), ...(requestToProcess.finallyData ?? [])]); } Log.info("[SequentialQueue] Removing persisted request because it failed and doesn't need to be retried.", false, {error, request: requestToProcess}); endPersistedRequestAndRemoveFromQueue(requestToProcess); From 13b0fb74fac37da04717b19801656e3dbf38cbbd Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 09:54:42 -0500 Subject: [PATCH 13/36] refactor: move change of card assign state to `finallyData` --- src/libs/actions/CompanyCards.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index a2e26c783ed58..60ab170db4f6b 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -358,11 +358,6 @@ function assignWorkspaceCompanyCard(policy: OnyxEntry, domainOrWorkspace key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`, value: {[optimisticCardAssignedReportAction.reportActionID]: {pendingAction: null}}, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ASSIGN_CARD, - value: {isAssigning: false, isAssignmentFinished: true}, - }, ], failureData: [ { @@ -375,11 +370,6 @@ function assignWorkspaceCompanyCard(policy: OnyxEntry, domainOrWorkspace }, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.ASSIGN_CARD, - value: {isAssigning: false, isAssignmentFinished: true}, - }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.FAILED_COMPANY_CARDS_ASSIGNMENTS}${domainOrWorkspaceAccountID}`, @@ -388,6 +378,13 @@ function assignWorkspaceCompanyCard(policy: OnyxEntry, domainOrWorkspace }, }, ], + finallyData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.ASSIGN_CARD, + value: {isAssigning: false, isAssignmentFinished: true}, + }, + ], }; API.write(WRITE_COMMANDS.ASSIGN_COMPANY_CARD, parameters, onyxData); From 5f10adcf177f425af8e8486280377914b57d150f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 10:14:36 -0500 Subject: [PATCH 14/36] fix: add back `domainOrWorkspaceAccountID` --- .../companyCards/WorkspaceCompanyCardsTable.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx index db521cb092ae4..b0401a1389f37 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx @@ -46,7 +46,15 @@ type WorkspaceCompanyCardsTableProps = { CardFeedIcon?: React.ReactNode; }; -function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAssigningCardDisabled, shouldShowGBDisclaimer, CardFeedIcon}: WorkspaceCompanyCardsTableProps) { +function WorkspaceCompanyCardsTable({ + selectedFeed, + policyID, + domainOrWorkspaceAccountID, + onAssignCard, + isAssigningCardDisabled, + shouldShowGBDisclaimer, + CardFeedIcon, +}: WorkspaceCompanyCardsTableProps) { const styles = useThemeStyles(); const {isOffline} = useNetwork(); const {translate, localeCompare} = useLocalize(); @@ -118,6 +126,7 @@ function WorkspaceCompanyCardsTable({selectedFeed, policyID, onAssignCard, isAss key={`${item.cardName}_${index}`} item={item} policyID={policyID} + domainOrWorkspaceAccountID={domainOrWorkspaceAccountID} CardFeedIcon={CardFeedIcon} isPlaidCardFeed={isPlaidCardFeed} onAssignCard={onAssignCard} From e5379f72d29aa0303576eeb9b371e20fdff0156d Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:01:58 -0500 Subject: [PATCH 15/36] re-arrange assign card flow --- src/ROUTES.ts | 20 +++---- src/SCREENS.ts | 4 +- src/libs/CardUtils.ts | 4 +- src/libs/Navigation/linkingConfig/config.ts | 4 +- src/libs/Navigation/types.ts | 3 +- .../BrokenCardFeedConnectionPage.tsx | 54 +++++++++++++++++++ ...orkspaceCompanyCardsTableHeaderButtons.tsx | 2 +- tests/ui/AssignCardFeedPage.tsx | 15 ++++-- 8 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 3a35eba8d683b..fc985dcb16c3b 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -24,7 +24,7 @@ import type AssertTypesNotEqual from './types/utils/AssertTypesNotEqual'; type WorkspaceCompanyCardsAssignCardParams = { policyID: string; feed: CompanyCardFeedWithDomainID; - cardID?: string; + cardID: string; }; // This is a file containing constants for all the routes we want to be able to go to @@ -2188,20 +2188,15 @@ const ROUTES = { route: 'workspaces/:policyID/company-cards/select-feed', getRoute: (policyID: string) => `workspaces/${policyID}/company-cards/select-feed` as const, }, - WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: { - route: 'workspaces/:policyID/company-cards/:feed/assign-card', - getRoute: ({feed, cardID, policyID}: WorkspaceCompanyCardsAssignCardParams, backTo?: string) => - // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/assign-card${cardID ? `?cardID=${cardID}` : ''}`, backTo), + WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION: { + route: 'workspaces/:policyID/company-cards/:feed/broken-card-feed-connection', + getRoute: (policyID: string, feed: string) => `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/broken-card-feed-connection` as const, }, WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE: { route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/assignee', 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/${encodeURIComponent(params.cardID)}/assignee`, - backTo, - ), + getUrlWithBackToParam(`workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/assignee`, backTo), }, WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION: { route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/card-selection', @@ -2222,10 +2217,7 @@ const ROUTES = { route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/confirmation', 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/${encodeURIComponent(params.cardID)}/confirmation`, - backTo, - ), + getUrlWithBackToParam(`workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/confirmation`, backTo), }, WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_INVITE_NEW_MEMBER: { route: 'workspaces/:policyID/company-cards/:feed/assign-card/:cardID/invite-new-member', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4737629fac1b8..b0f767d4b7d44 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -579,8 +579,10 @@ const SCREENS = { INITIAL: 'Workspace_Initial', PROFILE: 'Workspace_Overview', COMPANY_CARDS: 'Workspace_CompanyCards', - COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard', + COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION: 'Workspace_CompanyCards_BrokenCardFeedConnection', COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE: 'Workspace_CompanyCards_AssignCard_Assignee', + COMPANY_CARDS_ASSIGN_CARD_BANK_CONNECTION: 'Workspace_CompanyCards_AssignCard_Bank_Connection', + COMPANY_CARDS_ASSIGN_CARD_PLAID_CONNECTION: 'Workspace_CompanyCards_AssignCard_Plaid_Connection', COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION: 'Workspace_CompanyCards_AssignCard_Card_Selection', COMPANY_CARDS_ASSIGN_CARD_TRANSACTION_START_DATE: 'Workspace_CompanyCards_AssignCard_Transaction_Start_Date', COMPANY_CARDS_ASSIGN_CARD_CARD_NAME: 'Workspace_CompanyCards_AssignCard_Card_Name', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 66bcec08f0bb5..48e56179f62ea 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -638,8 +638,10 @@ function checkIfNewFeedConnected(prevFeedsData: CompanyFeeds, currentFeedsData: } function filterInactiveCards(cards: CardList | undefined): CardList { + const assignedCards = getAssignedCardFromCardList(cards ?? {}); + const closedStates = new Set([CONST.EXPENSIFY_CARD.STATE.CLOSED, CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED]); - return filterObject(cards ?? {}, (key, card) => !closedStates.has(card.state)); + return filterObject(assignedCards ?? {}, (_key, card) => !closedStates.has(card.state)); } function getAllCardsForWorkspace( diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 1b7dae8ad92a4..3d15587513832 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -761,8 +761,8 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_ADD_NEW.route, }, - [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { - path: ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.route, + [SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION.route, }, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index cdbf2e50cecf4..af66caf6e1887 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1199,10 +1199,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: { policyID: string; }; - [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { + [SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION]: { policyID: string; feed: CompanyCardFeedWithDomainID; - 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/BrokenCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx new file mode 100644 index 0000000000000..36270dede3190 --- /dev/null +++ b/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx @@ -0,0 +1,54 @@ +import React, {useEffect} from 'react'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import LoadingPage from '@pages/LoadingPage'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import {clearAssignCardStepAndData} from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import PlaidConnectionStep from './addNew/PlaidConnectionStep'; +import BankConnection from './BankConnection'; + +type AssignCardFeedPageProps = PlatformStackScreenProps & WithPolicyAndFullscreenLoadingProps; + +function BrokenCardFeedConnectionPage({route, policy}: AssignCardFeedPageProps) { + const feed = decodeURIComponent(route.params?.feed) as CompanyCardFeedWithDomainID; + const policyID = policy?.id; + + const {translate} = useLocalize(); + + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); + const currentStep = assignCard?.currentStep; + + useEffect(() => { + return () => { + clearAssignCardStepAndData(); + }; + }, []); + + switch (currentStep) { + case CONST.COMPANY_CARD.STEP.BANK_CONNECTION: + return ( + + ); + case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: + return ( + + ); + default: + return ; + } +} + +export default withPolicyAndFullscreenLoading(BrokenCardFeedConnectionPage); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx index 4b7cf08aecfe3..0b11ff42b8a55 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTableHeaderButtons.tsx @@ -75,7 +75,7 @@ function WorkspaceCompanyCardsTableHeaderButtons({policyID, selectedFeed, should const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${currentFeedData?.domainID}`, {canBeMissing: true}); const openBankConnection = () => - Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed}))); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION.getRoute(policyID, selectedFeed))); const secondaryActions = [ { diff --git a/tests/ui/AssignCardFeedPage.tsx b/tests/ui/AssignCardFeedPage.tsx index 83bbb8f152782..3ec6ca95ca0b7 100644 --- a/tests/ui/AssignCardFeedPage.tsx +++ b/tests/ui/AssignCardFeedPage.tsx @@ -12,7 +12,7 @@ import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; import Navigation from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AssignCardFeedPage from '@pages/workspace/companyCards/assignCard/AssignCardFeedPage'; +import AssignCardFeedPage from '@pages/workspace/companyCards/BrokenCardFeedConnectionPage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -71,14 +71,17 @@ jest.mock('@components/FormAlertWithSubmitButton', () => 'FormAlertWithSubmitBut const Stack = createPlatformStackNavigator(); // Renders the AssignCardFeedPage inside a navigation container with necessary providers. -const renderPage = (initialRouteName: typeof SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD, initialParams: SettingsNavigatorParamList[typeof SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]) => { +const renderPage = ( + initialRouteName: typeof SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE, + initialParams: SettingsNavigatorParamList[typeof SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE], +) => { return render( @@ -145,9 +148,10 @@ describe('AssignCardFeedPage', () => { }); // Render the page with the specified policyID and backTo param - const {unmount} = renderPage(SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD, { + const {unmount} = renderPage(SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE, { policyID: policy.id, feed: CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, + cardID: '1234', backTo: ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policy?.id, 1234), }); @@ -211,9 +215,10 @@ describe('AssignCardFeedPage', () => { }); }); // Render the page with the specified policyID and backTo param - const {unmount} = renderPage(SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD, { + const {unmount} = renderPage(SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE, { policyID: policy.id, feed: CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, + cardID: '1234', backTo: ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policy?.id, 1234), }); From 91b21bfadf3a749f64e1f43efefab1132c651122 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:22:27 -0500 Subject: [PATCH 16/36] fix: commercial cards not shown --- src/libs/CardUtils.ts | 33 ++++++++----------- .../ModalStackNavigators/index.tsx | 5 +-- 2 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 48e56179f62ea..7a2ca4be05800 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -195,8 +195,8 @@ function formatCardExpiration(expirationDateString: string) { * @param cardList - collection of assigned cards * @returns collection of assigned cards grouped by domain */ -function getDomainCards(cardList: OnyxEntry): Record { - const assignedCards = getAssignedCardFromCardList(cardList ?? {}); +function getDomainCards(cardsList: OnyxEntry): Record { + const {cardList: assignableCards, ...assignedCards} = cardsList ?? {}; // Check for domainName to filter out personal credit cards. const activeCards = Object.values(assignedCards).filter((card) => !!card?.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.some((element) => element === card.state)); @@ -637,11 +637,16 @@ function checkIfNewFeedConnected(prevFeedsData: CompanyFeeds, currentFeedsData: }; } -function filterInactiveCards(cards: CardList | undefined): CardList { - const assignedCards = getAssignedCardFromCardList(cards ?? {}); +function filterInactiveCards(cardsList: CardList | undefined) { + const {cardList = {}, ...assignedCards} = cardsList ?? {}; const closedStates = new Set([CONST.EXPENSIFY_CARD.STATE.CLOSED, CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED]); - return filterObject(assignedCards ?? {}, (_key, card) => !closedStates.has(card.state)); + const filteredAssignedCards = filterObject(assignedCards, (_key, card) => !closedStates.has(card.state)); + + return { + cardList, + ...filteredAssignedCards, + } as CardList; } function getAllCardsForWorkspace( @@ -668,19 +673,8 @@ function getAllCardsForWorkspace( return cards; } -/** - * Get assigned cards from card list by extracting the list of assignable cards from the card list - * - * @param cardList the card list - * @returns the assigned cards - */ -function getAssignedCardFromCardList(cardList: CardList) { - const {cardList: assignableCards, ...assignedCards} = cardList; - return assignedCards; -} - -function isSmartLimitEnabled(cardList: CardList) { - const assignedCards = getAssignedCardFromCardList(cardList); +function isSmartLimitEnabled(cardsList: CardList) { + const {cardList, ...assignedCards} = cardsList ?? {}; return Object.values(assignedCards).some((card) => card.nameValuePairs?.limitType === CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART); } @@ -791,7 +785,7 @@ function isExpensifyCardPendingAction(card?: Card, privatePersonalDetails?: Priv } function hasPendingExpensifyCardAction(cards: CardList | undefined, privatePersonalDetails?: PrivatePersonalDetails) { - const assignedCards = getAssignedCardFromCardList(cards ?? {}); + const {cardList, ...assignedCards} = cards ?? {}; return Object.values(assignedCards).some((card) => isExpensifyCardPendingAction(card, privatePersonalDetails)); } const isCurrencySupportedForECards = (currency?: string) => { @@ -960,7 +954,6 @@ export { COMPANY_CARD_BANK_ICON_NAMES, isMaskedCardNumberEqual, splitMaskedCardNumber, - getAssignedCardFromCardList, }; export type {CompanyCardFeedIcons, CompanyCardBankIcons}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 99d460af58704..ebdd6f9f58744 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -698,10 +698,11 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default, [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, [SCREENS.WORKSPACE.INVOICES_VERIFY_ACCOUNT]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesVerifyAccountPage').default, - [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: () => require('../../../../pages/workspace/companyCards/assignCard/AssignCardFeedPage').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION]: () => require('../../../../pages/workspace/companyCards/BrokenCardFeedConnectionPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE]: () => require('../../../../pages/workspace/companyCards/assignCard/AssigneeStep').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION]: () => require('../../../../pages/workspace/companyCards/assignCard/CardSelectionStep').default, - [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_TRANSACTION_START_DATE]: () => require('../../../../pages/workspace/companyCards/assignCard/TransactionStartDateStep').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_TRANSACTION_START_DATE]: () => + require('../../../../pages/workspace/companyCards/assignCard/TransactionStartDateStep').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_CARD_NAME]: () => require('../../../../pages/workspace/companyCards/assignCard/CardNameStep').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION]: () => require('../../../../pages/workspace/companyCards/assignCard/ConfirmationStep').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_INVITE_NEW_MEMBER]: () => require('../../../../pages/workspace/companyCards/assignCard/InviteNewMemberStep').default, From ba84eae4bbc6bf822568994f1b36455fa6167ca2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:26:08 -0500 Subject: [PATCH 17/36] fix: spell check --- src/components/Search/FilterDropdowns/DropdownButton.tsx | 8 ++++---- src/components/Table/TableFilterButtons/index.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/Search/FilterDropdowns/DropdownButton.tsx b/src/components/Search/FilterDropdowns/DropdownButton.tsx index 2b6286b20b79b..a2b54a29d4494 100644 --- a/src/components/Search/FilterDropdowns/DropdownButton.tsx +++ b/src/components/Search/FilterDropdowns/DropdownButton.tsx @@ -44,8 +44,8 @@ type DropdownButtonProps = { /** Button label style */ labelStyle?: StyleProp; - /** Carret wrapper style */ - carretWrapperStyle?: StyleProp; + /** Caret wrapper style */ + caretWrapperStyle?: StyleProp; /** Wrapper style for the outer view */ wrapperStyle?: StyleProp; @@ -58,7 +58,7 @@ const ANCHOR_ORIGIN = { vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, }; -function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medium = false, labelStyle, innerStyles, carretWrapperStyle, wrapperStyle}: DropdownButtonProps) { +function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medium = false, labelStyle, innerStyles, caretWrapperStyle, wrapperStyle}: DropdownButtonProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to distinguish RHL and narrow layout // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth} = useResponsiveLayout(); @@ -138,7 +138,7 @@ function DropdownButton({label, value, viewportOffsetTop, PopoverComponent, medi {...(medium ? {medium: true} : {small: true})} > diff --git a/src/components/Table/TableFilterButtons/index.tsx b/src/components/Table/TableFilterButtons/index.tsx index 64c6d0ad125b9..0ae6a83faca3c 100644 --- a/src/components/Table/TableFilterButtons/index.tsx +++ b/src/components/Table/TableFilterButtons/index.tsx @@ -124,7 +124,7 @@ function FilterItemRenderer({item}: FilterItemRendererProps) { innerStyles={[styles.gap2, shouldShowResponsiveLayout && styles.mw100]} wrapperStyle={shouldShowResponsiveLayout && styles.w100} labelStyle={styles.fontSizeLabel} - carretWrapperStyle={styles.gap2} + caretWrapperStyle={styles.gap2} medium /> ); From 4eb520ec429fd64645da554a18e7b8cdc36d3369 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:42:38 -0500 Subject: [PATCH 18/36] fix: ESLint and TS errors --- src/ROUTES.ts | 8 ++++---- src/hooks/useAssignCard.ts | 2 +- src/libs/CardUtils.ts | 10 +++++----- src/libs/Navigation/types.ts | 12 ++++++------ src/pages/settings/Wallet/PaymentMethodList.tsx | 7 ++++--- .../companyCards/assignCard/AssigneeStep.tsx | 12 +++++++----- .../companyCards/assignCard/CardNameStep.tsx | 5 +---- .../companyCards/assignCard/CardSelectionStep.tsx | 2 +- .../companyCards/assignCard/ConfirmationStep.tsx | 8 +++++--- .../companyCards/assignCard/InviteNewMemberStep.tsx | 2 +- .../assignCard/TransactionStartDateStep.tsx | 12 +----------- .../workspace/members/WorkspaceMemberDetailsPage.tsx | 4 +--- src/types/onyx/Card.ts | 7 ++----- 13 files changed, 39 insertions(+), 52 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index fc985dcb16c3b..b1c5752bf9085 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2169,7 +2169,7 @@ const ROUTES = { }, WORKSPACE_COMPANY_CARDS_BANK_CONNECTION: { route: 'workspaces/:policyID/company-cards/:feed/bank-connection', - getRoute: (policyID: string | undefined, feed: string, backTo: string) => { + getRoute: (policyID: string | undefined, feed: string, backTo?: string) => { if (!policyID) { Log.warn('Invalid policyID is used to build the WORKSPACE_COMPANY_CARDS_BANK_CONNECTION route'); } @@ -2225,11 +2225,11 @@ const ROUTES = { `workspaces/${params.policyID}/company-cards/${encodeURIComponent(params.feed)}/assign-card/${encodeURIComponent(params.cardID)}/invite-new-member` as const, }, WORKSPACE_COMPANY_CARD_DETAILS: { - route: 'workspaces/:policyID/company-cards/:bank/:cardID', + route: 'workspaces/:policyID/company-cards/:feed/:cardID', - getRoute: (policyID: string, cardID: string, bank: string, backTo?: string) => + getRoute: (policyID: string, cardID: string, feed: string, backTo?: string) => // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(bank)}/${encodeURIComponent(cardID)}`, backTo), + getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}`, backTo), }, WORKSPACE_COMPANY_CARD_NAME: { route: 'workspaces/:policyID/company-cards/:bank/:cardID/edit/name', diff --git a/src/hooks/useAssignCard.ts b/src/hooks/useAssignCard.ts index 8e7303c25bc97..5936ef64b7ae5 100644 --- a/src/hooks/useAssignCard.ts +++ b/src/hooks/useAssignCard.ts @@ -109,7 +109,7 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA switch (initialStep) { case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: case CONST.COMPANY_CARD.STEP.BANK_CONNECTION: - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed, cardID})); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BANK_CONNECTION.getRoute(policyID, selectedFeed)); break; case CONST.COMPANY_CARD.STEP.ASSIGNEE: default: diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 7a2ca4be05800..93293be77bc08 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -637,7 +637,7 @@ function checkIfNewFeedConnected(prevFeedsData: CompanyFeeds, currentFeedsData: }; } -function filterInactiveCards(cardsList: CardList | undefined) { +function filterInactiveCards(cardsList: WorkspaceCardsList | undefined) { const {cardList = {}, ...assignedCards} = cardsList ?? {}; const closedStates = new Set([CONST.EXPENSIFY_CARD.STATE.CLOSED, CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED]); @@ -646,7 +646,7 @@ function filterInactiveCards(cardsList: CardList | undefined) { return { cardList, ...filteredAssignedCards, - } as CardList; + } as WorkspaceCardsList; } function getAllCardsForWorkspace( @@ -655,7 +655,7 @@ function getAllCardsForWorkspace( cardFeeds?: CombinedCardFeeds, expensifyCardSettings?: OnyxCollection, ): CardList { - const cards = {}; + const cards: CardList = {}; const companyCardsDomainFeeds = Object.entries(cardFeeds ?? {}).map(([feedName, feedData]) => ({domainID: feedData.domainID, feedName})); const expensifyCardsDomainIDs = Object.keys(expensifyCardSettings ?? {}) .map((key) => key.split('_').at(-1)) @@ -665,8 +665,8 @@ function getAllCardsForWorkspace( const isCompanyDomainCards = companyCardsDomainFeeds?.some((domainFeed) => domainFeed.domainID && key.includes(domainFeed.domainID.toString()) && key.includes(domainFeed.feedName)); const isExpensifyDomainCards = expensifyCardsDomainIDs.some((domainID) => key.includes(domainID.toString()) && key.includes(CONST.EXPENSIFY_CARD.BANK)); if ((isWorkspaceAccountCards || isCompanyDomainCards || isExpensifyDomainCards) && values) { - const {cardList, ...rest} = values; - const filteredCards = filterInactiveCards(rest); + const {cardList: assignableCards, ...assignedCards} = values ?? {}; + const filteredCards = filterInactiveCards(assignedCards); Object.assign(cards, filteredCards); } } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index af66caf6e1887..ac14812e71885 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1208,7 +1208,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; 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 @@ -1216,22 +1216,22 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; cardID: string; }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_TRANSACTION_START_DATE]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; cardID: string; }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_CARD_NAME]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; cardID: string; }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; 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 @@ -1239,7 +1239,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_INVITE_NEW_MEMBER]: { policyID: string; - feed: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; cardID: string; }; [SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME]: { diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index c511d7d842811..49b69e5161c07 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -378,20 +378,21 @@ function PaymentMethodList({ isLoadingBankAccountList, bankAccountList, styles, + translate, isOffline, filterType, filterCurrency, isLoadingCardList, cardList, illustrations, - translate, - getCardBrickRoadIndicator, + companyCardFeedIcons, onPress, + getCardBrickRoadIndicator, shouldShowRightIcon, itemIconRight, + expensifyIcons.ThreeDots, activePaymentMethodID, actionPaymentMethodType, - expensifyIcons.ThreeDots, onThreeDotsMenuPress, ]); diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 996e78d80b499..3d03ad90aefb0 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -2,7 +2,6 @@ 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 InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import SelectionList from '@components/SelectionList'; import UserListItem from '@components/SelectionList/ListItem/UserListItem'; @@ -29,8 +28,7 @@ import {setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; +import type SCREENS from '@src/SCREENS'; import type {AssignCardData} from '@src/types/onyx/AssignCard'; type AssigneeStepProps = { @@ -92,7 +90,9 @@ function AssigneeStep({route}: AssigneeStepProps) { data.encryptedCardNumber = assignCard.data.encryptedCardNumber; data.cardNumber = assignCard.data.cardNumber; data.startDate = !isEditing ? format(new Date(), CONST.DATE.FNS_FORMAT_STRING) : (assignCard?.data?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); - data.dateOption = !isEditing ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM : (assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); + data.dateOption = !isEditing + ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM + : (assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); setAssignCardStepAndData({ data, isEditing: false, @@ -124,7 +124,9 @@ function AssigneeStep({route}: AssigneeStepProps) { data.encryptedCardNumber = assignCard.data.encryptedCardNumber; data.cardNumber = assignCard.data.cardNumber; data.startDate = !isEditing ? format(new Date(), CONST.DATE.FNS_FORMAT_STRING) : (assignCard?.data?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); - data.dateOption = !isEditing ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM : (assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); + data.dateOption = !isEditing + ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM + : (assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); setAssignCardStepAndData({ data, isEditing: false, diff --git a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx index 0bdc4528f18ac..89a4ec6b42582 100644 --- a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx @@ -19,8 +19,7 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/EditExpensifyCardNameForm'; type CardNameStepProps = PlatformStackScreenProps; @@ -32,8 +31,6 @@ function CardNameStep({route}: CardNameStepProps) { const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); const policyID = route.params.policyID; - const feed = route.params.feed; - const cardID = route.params.cardID; const data = assignCard?.data; diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index d0ecdf263f7d8..5228dbd9611b4 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -30,7 +30,7 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; type CardSelectionStepProps = PlatformStackScreenProps; diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx index 0d34f101e7c0d..bffd42679a16f 100644 --- a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx @@ -28,7 +28,7 @@ import {assignWorkspaceCompanyCard, clearAssignCardStepAndData, setAddNewCompany import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; +import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID, CurrencyList} from '@src/types/onyx'; import {getEmptyObject} from '@src/types/utils/EmptyObject'; @@ -64,7 +64,7 @@ function ConfirmationStep({route}: ConfirmationStepProps) { const cardholderEmail = data?.email ?? ''; const cardholderAccountID = cardholder?.accountID; - const currentFullScreenRoute = useRootNavigationState((state) => state?.routes?.findLast((route) => isFullScreenName(route.name))); + const currentFullScreenRoute = useRootNavigationState((state) => state?.routes?.findLast((currentRoute) => isFullScreenName(currentRoute.name))); useEffect(() => { if (!assignCard?.isAssignmentFinished) { @@ -100,7 +100,7 @@ function ConfirmationStep({route}: ConfirmationStepProps) { }); } // For expired feeds, navigate to the old ASSIGN_CARD route which handles these special cases - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed, cardID})); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BANK_CONNECTION.getRoute(policyID, feed)); return; } assignWorkspaceCompanyCard(policy, domainOrWorkspaceAccountID, {...data, cardholder, bankName}); @@ -121,6 +121,8 @@ function ConfirmationStep({route}: ConfirmationStepProps) { case CONST.COMPANY_CARD.STEP.CARD_NAME: Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_NAME.getRoute(routeParams)); break; + default: + throw new Error(`Invalid step: ${step}`); } }; diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx index 35b9355655c3d..aa0c22eb118a5 100644 --- a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -22,7 +22,7 @@ import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; type InviteeNewMemberStepProps = Omit & WithCurrentUserPersonalDetailsProps & { - route: PlatformStackRouteProp; + route: PlatformStackRouteProp; /** Selected feed */ feed: CompanyCardFeedWithDomainID; }; diff --git a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx index 4dee06c437fb3..8086c60b08d2f 100644 --- a/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/TransactionStartDateStep.tsx @@ -1,5 +1,4 @@ import {format, subDays} from 'date-fns'; -import {Str} from 'expensify-common'; import React, {useState} from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; @@ -11,25 +10,16 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {isRequiredFulfilled} from '@libs/ValidationUtils'; import Navigation from '@navigation/Navigation'; import {setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; -function TransactionStartDateStep({route}: {route: PlatformStackRouteProp}) { +function TransactionStartDateStep() { const {translate} = useLocalize(); const styles = useThemeStyles(); - const policyID = route.params.policyID; - const feed = route.params.feed; - const cardID = route.params.cardID; - const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); const isEditing = assignCard?.isEditing; const data = assignCard?.data; diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 93cc1ee831eda..aabec2dd5b4df 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -8,8 +8,6 @@ import Button from '@components/Button'; import ButtonDisabledWhenOffline from '@components/Button/ButtonDisabledWhenOffline'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -// eslint-disable-next-line no-restricted-imports -import * as Expensicons from '@components/Icon/Expensicons'; import {LockedAccountContext} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -103,7 +101,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const ownerDetails = useMemo(() => personalDetails?.[policy?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID] ?? ({} as PersonalDetails), [personalDetails, policy?.ownerAccountID]); const policyOwnerDisplayName = formatPhoneNumber(getDisplayNameOrDefault(ownerDetails)) ?? policy?.owner ?? ''; const hasMultipleFeeds = Object.keys(getCompanyFeeds(cardFeeds, false, true)).length > 0; - const workspaceCards = getAllCardsForWorkspace(workspaceAccountID, cardList, cardFeeds, expensifyCardSettings); + const {cardList: assignableCards, ...workspaceCards} = getAllCardsForWorkspace(workspaceAccountID, cardList, cardFeeds, expensifyCardSettings); const isSMSLogin = Str.isSMSLogin(memberLogin); const phoneNumber = getPhoneNumber(details); const isReimburser = policy?.achAccount?.reimburser === memberLogin; diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index def76213b3fe4..73f65a038a030 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -224,10 +224,7 @@ type ExpensifyCardDetails = { type AssignableCardsList = Record; /** Record of Company or Expensify cards, indexed by cardID */ -type CardList = Record & { - /** List of assignable cards */ - cardList?: AssignableCardsList; -}; +type CardList = Record; /** Issue new card flow steps */ type IssueNewCardStep = ValueOf; @@ -287,7 +284,7 @@ type IssueNewCard = { }; /** List of Expensify cards */ -type WorkspaceCardsList = Record & { +type WorkspaceCardsList = CardList & { /** List of cards to assign */ cardList?: Record; }; From 1616e5c0aa6053941e320792d880c677f67ba168 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:47:25 -0500 Subject: [PATCH 19/36] fix: invalid type --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 0b86ef255065c..5a0e01a568446 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2227,7 +2227,7 @@ const ROUTES = { WORKSPACE_COMPANY_CARD_DETAILS: { route: 'workspaces/:policyID/company-cards/:feed/:cardID', - getRoute: (policyID: string, cardID: string, feed: WorkspaceCardFeedWithDomainID, backTo?: string) => + getRoute: (policyID: string, cardID: string, feed: CompanyCardFeedWithDomainID, backTo?: string) => // eslint-disable-next-line no-restricted-syntax -- Legacy route generation getUrlWithBackToParam(`workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/${encodeURIComponent(cardID)}`, backTo), }, From 6f9b8944c49d9c2b068431aba8625ebe6c0eff9f Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:51:52 -0500 Subject: [PATCH 20/36] fix: do not inline cardFeedIcon element --- .../expensifyCard/WorkspaceExpensifyCardListPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index 2e632f8ab5505..2e6959c4a6c2c 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -88,6 +88,8 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp const headerHeight = useEmptyViewHeaderHeight(shouldUseNarrowLayout, isBankAccountVerified); const [footerHeight, setFooterHeight] = useState(0); + const cardFeedIcon = useMemo(() => , []); + const settlementCurrency = useCurrencyForExpensifyCard({policyID}); const allCards = useMemo(() => { @@ -234,7 +236,7 @@ function WorkspaceExpensifyCardListPage({route, cardsList, fundID}: WorkspaceExp Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_SELECT_FEED.getRoute(policyID))} - CardFeedIcon={} + CardFeedIcon={cardFeedIcon} feedName={translate('workspace.common.expensifyCard')} supportingText={getDescriptionForPolicyDomainCard(cardSettings?.domainName ?? '')} /> From 61f9fd7ab34dccd7c2a66383c956f750c8097033 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 11:51:56 -0500 Subject: [PATCH 21/36] fix: run prettier --- .../WorkspaceInviteMessageComponent.tsx | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index e39fa72b2debf..79f5de168d9d7 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -23,16 +23,16 @@ import {clearDraftValues} from '@libs/actions/FormActions'; import {openExternalLink} from '@libs/actions/Link'; import {addMembersToWorkspace, clearWorkspaceInviteRoleDraft} from '@libs/actions/Policy/Member'; import {setWorkspaceInviteMessageDraft} from '@libs/actions/Policy/Policy'; -import {setAssignCardStepAndData} from '@userActions/CompanyCards'; -import {clearInviteDraft} from '@userActions/Policy/Member'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Navigation from '@libs/Navigation/Navigation'; import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import {getDisplayNameOrDefault, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; -import {getDefaultAvatarURL} from '@libs/UserAvatarUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; +import {getDefaultAvatarURL} from '@libs/UserAvatarUtils'; import variables from '@styles/variables'; +import {setAssignCardStepAndData} from '@userActions/CompanyCards'; +import {clearInviteDraft} from '@userActions/Policy/Member'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import AccessOrNotFoundWrapper from '@src/pages/workspace/AccessOrNotFoundWrapper'; @@ -252,14 +252,14 @@ function WorkspaceInviteMessageComponent({ - {isInviteNewMemberStep && ( - - )} + {isInviteNewMemberStep && ( + + )} {shouldShowMemberNames && !isInviteNewMemberStep && ( Date: Fri, 19 Dec 2025 12:12:30 -0500 Subject: [PATCH 22/36] fix: TS errors --- src/libs/Navigation/types.ts | 2 +- src/libs/Network/SequentialQueue.ts | 3 ++- .../workspace/companyCards/assignCard/AssigneeStep.tsx | 7 ++----- .../companyCards/assignCard/CardSelectionStep.tsx | 2 +- .../workspace/companyCards/assignCard/ConfirmationStep.tsx | 4 ++-- .../workspace/members/WorkspaceInviteMessageComponent.tsx | 1 - 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ac14812e71885..60dec4f299165 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1149,7 +1149,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: { policyID: string; - bankName: string; + bankName: CompanyCardFeedWithDomainID; // 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/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index a3707e431ec0a..ed85e6662ac05 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -170,7 +170,8 @@ function process(): Promise { // Duplicate records don't need to be retried as they just mean the record already exists on the server if (error.name === CONST.ERROR.REQUEST_CANCELLED || error.message === CONST.ERROR.DUPLICATE_RECORD || shouldFailAllRequests) { if (shouldFailAllRequests) { - Onyx.update([...(requestToProcess.failureData ?? []), ...(requestToProcess.finallyData ?? [])]); + const onyxUpdates = [...(requestToProcess.failureData ?? []), ...(requestToProcess.finallyData ?? [])] as OnyxUpdate[]; + Onyx.update(onyxUpdates); } Log.info("[SequentialQueue] Removing persisted request because it failed and doesn't need to be retried.", false, {error, request: requestToProcess}); endPersistedRequestAndRemoveFromQueue(requestToProcess); diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 3d03ad90aefb0..366780f626d14 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -17,7 +17,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftInviteAccountID} from '@libs/actions/Card'; import {searchInServer} from '@libs/actions/Report'; import {getDefaultCardName} from '@libs/CardUtils'; -import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getHeaderMessage, getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; @@ -31,10 +31,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {AssignCardData} from '@src/types/onyx/AssignCard'; -type AssigneeStepProps = { - /** Route params */ - route: PlatformStackRouteProp; -}; +type AssigneeStepProps = PlatformStackScreenProps; function AssigneeStep({route}: AssigneeStepProps) { const policyID = route.params.policyID; diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index 5228dbd9611b4..3b988c61eb5fa 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -37,7 +37,7 @@ type CardSelectionStepProps = PlatformStackScreenProps; function ConfirmationStep({route}: ConfirmationStepProps) { const policyID = route.params.policyID; - const feed = route.params.feed as CompanyCardFeedWithDomainID; + const feed = route.params.feed; const cardID = route.params.cardID; const {translate} = useLocalize(); const styles = useThemeStyles(); diff --git a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx index 79f5de168d9d7..e09be4aec02f4 100644 --- a/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx +++ b/src/pages/workspace/members/WorkspaceInviteMessageComponent.tsx @@ -29,7 +29,6 @@ import {getPersonalDetailsForAccountIDs} from '@libs/OptionsListUtils'; import {getDisplayNameOrDefault, getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getMemberAccountIDsForWorkspace, goBackFromInvalidPolicy} from '@libs/PolicyUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; -import {getDefaultAvatarURL} from '@libs/UserAvatarUtils'; import variables from '@styles/variables'; import {setAssignCardStepAndData} from '@userActions/CompanyCards'; import {clearInviteDraft} from '@userActions/Policy/Member'; From 7a8ee1fd709c353ab48540b3b60c0e1bf14d6b46 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:20:44 -0500 Subject: [PATCH 23/36] fix: more TS errors --- .../companyCards/BrokenCardFeedConnectionPage.tsx | 8 ++++---- src/pages/workspace/withPolicy.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx index 36270dede3190..b87a86799c4f3 100644 --- a/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx @@ -10,14 +10,14 @@ import {clearAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import PlaidConnectionStep from './addNew/PlaidConnectionStep'; import BankConnection from './BankConnection'; -type AssignCardFeedPageProps = PlatformStackScreenProps & WithPolicyAndFullscreenLoadingProps; +type BrokenCardFeedConnectionPageProps = PlatformStackScreenProps & + WithPolicyAndFullscreenLoadingProps; -function BrokenCardFeedConnectionPage({route, policy}: AssignCardFeedPageProps) { - const feed = decodeURIComponent(route.params?.feed) as CompanyCardFeedWithDomainID; +function BrokenCardFeedConnectionPage({route, policy}: BrokenCardFeedConnectionPageProps) { + const feed = route.params?.feed; const policyID = policy?.id; const {translate} = useLocalize(); diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 618ddc20cd5cc..1245cce7ef6b3 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -50,7 +50,7 @@ type PolicyRouteName = | typeof SCREENS.WORKSPACE.ACCOUNTING.CARD_RECONCILIATION | typeof SCREENS.WORKSPACE.RULES | typeof SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW - | typeof SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD; + | typeof SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION; type PolicyRoute = PlatformStackRouteProp; From dd7aa13db0d6d4461e48f24d977bce506ce320c2 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:20:58 -0500 Subject: [PATCH 24/36] refactor: rename arbitrary `data` property to `cardToAssign` --- src/hooks/useAssignCard.ts | 20 ++++----- src/hooks/useImportPlaidAccounts.ts | 8 ++-- src/libs/actions/CompanyCards.ts | 4 +- src/libs/actions/IOU.ts | 16 +++---- src/libs/actions/Report.ts | 4 +- src/libs/actions/Search.ts | 14 +++---- .../BankConnection/index.native.tsx | 6 +-- .../companyCards/BankConnection/index.tsx | 6 +-- .../addNew/PlaidConnectionStep.tsx | 4 +- .../companyCards/assignCard/AssigneeStep.tsx | 38 ++++++++--------- .../companyCards/assignCard/CardNameStep.tsx | 4 +- .../assignCard/CardSelectionStep.tsx | 6 +-- .../assignCard/ConfirmationStep.tsx | 8 ++-- .../assignCard/InviteNewMemberStep.tsx | 42 +++++++++---------- .../assignCard/TransactionStartDateStep.tsx | 4 +- .../WorkspaceInviteMessageComponent.tsx | 4 +- .../members/WorkspaceMemberNewCardPage.tsx | 2 +- src/types/onyx/AssignCard.ts | 2 +- tests/ui/AssignCardFeedPage.tsx | 6 +-- 19 files changed, 99 insertions(+), 99 deletions(-) diff --git a/src/hooks/useAssignCard.ts b/src/hooks/useAssignCard.ts index 5936ef64b7ae5..a546321f7867d 100644 --- a/src/hooks/useAssignCard.ts +++ b/src/hooks/useAssignCard.ts @@ -99,9 +99,9 @@ function useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}: UseA return; } - const {initialStep, initialStepData} = initialAssignCardStep; + const {initialStep, cardToAssign} = initialAssignCardStep; - setAssignCardStepAndData({currentStep: initialStep, data: initialStepData}); + setAssignCardStepAndData({currentStep: initialStep, cardToAssign}); Navigation.setNavigationActionToMicrotaskQueue(() => { const routeParams = {policyID, feed: selectedFeed, cardID: cardID ?? ''}; @@ -145,12 +145,12 @@ function useInitialAssignCardStep({policyID, selectedFeed}: UseInitialAssignCard const isFeedExpired = isSelectedFeedExpired(feedData); const plaidAccessToken = feedData?.plaidAccessToken; - const getInitialAssignCardStep = (cardID: string | undefined) => { + const getInitialAssignCardStep = (cardID: string | undefined): {initialStep: AssignCardStep; cardToAssign: Partial} | undefined => { if (!selectedFeed) { return; } - const data: Partial = { + const cardToAssign: Partial = { bankName, cardNumber: cardID, encryptedCardNumber: cardID, @@ -174,33 +174,33 @@ function useInitialAssignCardStep({policyID, selectedFeed}: UseInitialAssignCard return { initialStep: CONST.COMPANY_CARD.STEP.PLAID_CONNECTION, - initialStepData: data, + cardToAssign, }; } return { initialStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION, - initialStepData: data, + cardToAssign, }; } const employeeList = Object.values(policy?.employeeList ?? {}).filter((employee) => !isDeletedPolicyEmployee(employee, isOffline)); if (employeeList.length === 1) { const userEmail = Object.keys(policy?.employeeList ?? {}).at(0) ?? ''; - data.email = userEmail; + cardToAssign.email = userEmail; const personalDetails = getPersonalDetailByEmail(userEmail); const memberName = personalDetails?.firstName ? personalDetails.firstName : personalDetails?.login; - data.cardName = `${memberName}'s card`; + cardToAssign.cardName = `${memberName}'s card`; return { initialStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, - initialStepData: data, + cardToAssign, }; } return { initialStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, - initialStepData: data, + cardToAssign, }; }; diff --git a/src/hooks/useImportPlaidAccounts.ts b/src/hooks/useImportPlaidAccounts.ts index da3a7b697dc0e..c72ff2c70ae2e 100644 --- a/src/hooks/useImportPlaidAccounts.ts +++ b/src/hooks/useImportPlaidAccounts.ts @@ -8,10 +8,10 @@ export default function useImportPlaidAccounts(policyID?: string) { const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); - const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.data?.plaidAccessToken; - const plaidFeed = addNewCard?.data?.plaidConnectedFeed ?? assignCard?.data?.institutionId; - const plaidFeedName = addNewCard?.data?.plaidConnectedFeedName ?? assignCard?.data?.plaidConnectedFeedName; - const plaidAccounts = addNewCard?.data?.plaidAccounts ?? assignCard?.data?.plaidAccounts; + const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; + const plaidFeed = addNewCard?.data?.plaidConnectedFeed ?? assignCard?.cardToAssign?.institutionId; + const plaidFeedName = addNewCard?.data?.plaidConnectedFeedName ?? assignCard?.cardToAssign?.plaidConnectedFeedName; + const plaidAccounts = addNewCard?.data?.plaidAccounts ?? assignCard?.cardToAssign?.plaidAccounts; const country = addNewCard?.data?.selectedCountry; const statementPeriodEnd = addNewCard?.data?.statementPeriodEnd; const statementPeriodEndDay = addNewCard?.data?.statementPeriodEndDay; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 60ab170db4f6b..7376d7822e786 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -47,8 +47,8 @@ type AddNewCompanyCardFlowData = { data?: Partial; }; -function setAssignCardStepAndData({data, isEditing, currentStep}: Partial) { - Onyx.merge(ONYXKEYS.ASSIGN_CARD, {data, isEditing, currentStep}); +function setAssignCardStepAndData({cardToAssign: cardToAssign, isEditing, currentStep}: Partial) { + Onyx.merge(ONYXKEYS.ASSIGN_CARD, {cardToAssign: cardToAssign, isEditing, currentStep}); } function setTransactionStartDate(startDate: string) { diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index c6095be3feeba..719398959365e 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4897,7 +4897,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]: violationsOnyxData.value, }, }, @@ -4907,7 +4907,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]: currentTransactionViolations, }, }, @@ -9571,7 +9571,7 @@ function deleteMoneyRequest({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`]: { // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 pendingFields: { @@ -12363,7 +12363,7 @@ function replaceReceipt({transactionID, file, source, transactionPolicy, transac onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { receipt: receiptOptimistic, }, @@ -12376,7 +12376,7 @@ function replaceReceipt({transactionID, file, source, transactionPolicy, transac onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { receipt: !isEmptyObject(oldReceipt) ? oldReceipt : null, }, @@ -13503,7 +13503,7 @@ function getSearchOnyxUpdate({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, value: { - data: { + cardToAssign: { [ONYXKEYS.PERSONAL_DETAILS_LIST]: { [toAccountID]: { accountID: toAccountID, @@ -15046,7 +15046,7 @@ function updateSplitTransactions({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContext?.currentSearchHash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: null, }, }, @@ -15057,7 +15057,7 @@ function updateSplitTransactions({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContext?.currentSearchHash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: originalTransaction, }, }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index baa920755cbdc..163f072417867 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2967,7 +2967,7 @@ function buildNewReportOptimisticData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, value: { - data: {[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: optimisticReportData}, + cardToAssign: {[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: optimisticReportData}, }, }); } @@ -6000,7 +6000,7 @@ function buildOptimisticChangePolicyData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, value: { - data: {[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: policy}, + cardToAssign: {[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: policy}, }, }); } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 9452647647427..91f735f60729a 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -255,7 +255,7 @@ function getOnyxLoadingData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - ...(isOffline ? {} : {data: []}), + ...(isOffline ? {} : {cardToAssign: []}), search: { status: queryJSON?.status, type: queryJSON?.type, @@ -468,7 +468,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: Report[], policy: onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, null])), + cardToAssign: Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, null])), }, }); } @@ -524,7 +524,7 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], curre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), + cardToAssign: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), }, }); } @@ -589,7 +589,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, }, }, @@ -651,7 +651,7 @@ function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], curre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])), + cardToAssign: Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])), }, }); } @@ -702,7 +702,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: { + cardToAssign: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, }, }, @@ -714,7 +714,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - data: { + cardToAssign: { // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {pendingAction: null}, diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 29fe8c011e4a2..9825abac1eb9c 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -55,7 +55,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti const policyID = policyIDFromProps ?? policyIDFromRoute; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); const {isBetaEnabled} = usePermissions(); - const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.data?.plaidAccessToken; + const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; const isPlaid = isBetaEnabled(CONST.BETAS.PLAID_COMPANY_CARDS) && !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); @@ -121,7 +121,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti return; } setAssignCardStepAndData({ - currentStep: assignCard?.data?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, + currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, isEditing: false, }); return; @@ -153,7 +153,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti url, feed, isFeedExpired, - assignCard?.data?.dateOption, + assignCard?.cardToAssign?.dateOption, isPlaid, onImportPlaidAccounts, isFeedConnectionBroken, diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index eb8c304cc880b..e911a95e46b42 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -64,7 +64,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); const {isOffline} = useNetwork(); - const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.data?.plaidAccessToken; + const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const {isBetaEnabled} = usePermissions(); const isPlaid = isBetaEnabled(CONST.BETAS.PLAID_COMPANY_CARDS) && !!plaidToken; @@ -141,7 +141,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti return; } setAssignCardStepAndData({ - currentStep: assignCard?.data?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, + currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, isEditing: false, }); return; @@ -195,7 +195,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti feed, isFeedExpired, isOffline, - assignCard?.data?.dateOption, + assignCard?.cardToAssign?.dateOption, isPlaid, onImportPlaidAccounts, isFeedConnectionBroken, diff --git a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx index 9b77acdc24780..7c58ca3669017 100644 --- a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx +++ b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx @@ -137,7 +137,7 @@ function PlaidConnectionStep({feed, policyID, onExit}: {feed?: CompanyCardFeedWi // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { setAssignCardStepAndData({ - data: { + cardToAssign: { plaidAccessToken: publicToken, institutionId: plaidConnectedFeed, plaidConnectedFeedName, @@ -149,7 +149,7 @@ function PlaidConnectionStep({feed, policyID, onExit}: {feed?: CompanyCardFeedWi return; } setAssignCardStepAndData({ - data: { + cardToAssign: { plaidAccessToken: publicToken, institutionId: plaidConnectedFeed, plaidConnectedFeedName, diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 366780f626d14..7ac61ce0bcb50 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -82,23 +82,23 @@ function AssigneeStep({route}: AssigneeStepProps) { const routeParams = {policyID, feed, cardID}; - if (assignee?.login === assignCard?.data?.email) { - if (assignCard?.data?.encryptedCardNumber) { - data.encryptedCardNumber = assignCard.data.encryptedCardNumber; - data.cardNumber = assignCard.data.cardNumber; - data.startDate = !isEditing ? format(new Date(), CONST.DATE.FNS_FORMAT_STRING) : (assignCard?.data?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); + if (assignee?.login === assignCard?.cardToAssign?.email) { + if (assignCard?.cardToAssign?.encryptedCardNumber) { + data.encryptedCardNumber = assignCard.cardToAssign.encryptedCardNumber; + data.cardNumber = assignCard.cardToAssign.cardNumber; + data.startDate = !isEditing ? format(new Date(), CONST.DATE.FNS_FORMAT_STRING) : (assignCard?.cardToAssign?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); data.dateOption = !isEditing ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM - : (assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); + : (assignCard?.cardToAssign?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); setAssignCardStepAndData({ - data, + cardToAssign: data, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION.getRoute(routeParams, backTo)); return; } setAssignCardStepAndData({ - data, + cardToAssign: data, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION.getRoute(routeParams)); @@ -107,7 +107,7 @@ function AssigneeStep({route}: AssigneeStepProps) { if (!policy?.employeeList?.[assignee?.login ?? '']) { setAssignCardStepAndData({ - data: { + cardToAssign: { invitingMemberEmail: assignee?.login ?? '', invitingMemberAccountID: assignee?.accountID ?? undefined, }, @@ -117,22 +117,22 @@ function AssigneeStep({route}: AssigneeStepProps) { return; } - if (assignCard?.data?.encryptedCardNumber) { - data.encryptedCardNumber = assignCard.data.encryptedCardNumber; - data.cardNumber = assignCard.data.cardNumber; - data.startDate = !isEditing ? format(new Date(), CONST.DATE.FNS_FORMAT_STRING) : (assignCard?.data?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); + if (assignCard?.cardToAssign?.encryptedCardNumber) { + data.encryptedCardNumber = assignCard.cardToAssign.encryptedCardNumber; + data.cardNumber = assignCard.cardToAssign.cardNumber; + data.startDate = !isEditing ? format(new Date(), CONST.DATE.FNS_FORMAT_STRING) : (assignCard?.cardToAssign?.startDate ?? format(new Date(), CONST.DATE.FNS_FORMAT_STRING)); data.dateOption = !isEditing ? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM - : (assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); + : (assignCard?.cardToAssign?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM); setAssignCardStepAndData({ - data, + cardToAssign: data, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION.getRoute(routeParams, backTo)); return; } setAssignCardStepAndData({ - data, + cardToAssign: data, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION.getRoute(routeParams)); @@ -165,7 +165,7 @@ function AssigneeStep({route}: AssigneeStepProps) { alternateText: email, login: email, accountID: personalDetail?.accountID, - isSelected: assignCard?.data?.email === email, + isSelected: assignCard?.cardToAssign?.email === email, icons: [ { source: personalDetail?.avatar ?? icons.FallbackAvatar, @@ -180,7 +180,7 @@ function AssigneeStep({route}: AssigneeStepProps) { membersList = sortAlphabetically(membersList, 'text', localeCompare); return membersList; - }, [isOffline, policy?.employeeList, assignCard?.data?.email, formatPhoneNumber, localeCompare, icons.FallbackAvatar]); + }, [isOffline, policy?.employeeList, assignCard?.cardToAssign?.email, formatPhoneNumber, localeCompare, icons.FallbackAvatar]); const assignees = useMemo(() => { if (!debouncedSearchTerm) { @@ -253,7 +253,7 @@ function AssigneeStep({route}: AssigneeStepProps) { onSelectRow={submit} ListItem={UserListItem} textInputOptions={textInputOptions} - initiallyFocusedItemKey={assignCard?.data?.email} + initiallyFocusedItemKey={assignCard?.cardToAssign?.email} showLoadingPlaceholder={!areOptionsInitialized} isLoadingNewOptions={!!isSearchingForReports} disableMaintainingScrollPosition diff --git a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx index 89a4ec6b42582..c37cd09f5ebbd 100644 --- a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx @@ -32,11 +32,11 @@ function CardNameStep({route}: CardNameStepProps) { const policyID = route.params.policyID; - const data = assignCard?.data; + const data = assignCard?.cardToAssign; const submit = (values: FormOnyxValues) => { setAssignCardStepAndData({ - data: { + cardToAssign: { cardName: values.name, }, isEditing: false, diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index 3b988c61eb5fa..c5e0fe4fe982e 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -52,10 +52,10 @@ function CardSelectionStep({route}: CardSelectionStepProps) { const plaidUrl = getPlaidInstitutionIconUrl(feed); const isEditing = assignCard?.isEditing; - const assigneeDisplayName = Str.removeSMSDomain(getPersonalDetailByEmail(assignCard?.data?.email ?? '')?.displayName ?? ''); + const assigneeDisplayName = Str.removeSMSDomain(getPersonalDetailByEmail(assignCard?.cardToAssign?.email ?? '')?.displayName ?? ''); const filteredCardList = getFilteredCardList(list, cardFeeds?.[feed]?.accountList, workspaceCardFeeds); - const [cardSelected, setCardSelected] = useState(assignCard?.data?.encryptedCardNumber ?? ''); + const [cardSelected, setCardSelected] = useState(assignCard?.cardToAssign?.encryptedCardNumber ?? ''); const [shouldShowError, setShouldShowError] = useState(false); const cardListOptions = Object.entries(filteredCardList).map(([cardNumber, encryptedCardNumber]) => ({ @@ -107,7 +107,7 @@ function CardSelectionStep({route}: CardSelectionStepProps) { ?.at(0) ?? ''; setAssignCardStepAndData({ - data: {encryptedCardNumber: cardSelected, cardNumber}, + cardToAssign: {encryptedCardNumber: cardSelected, cardNumber}, isEditing: false, }); diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx index c49ef91580eb0..872a38dfa769f 100644 --- a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx @@ -43,13 +43,13 @@ function ConfirmationStep({route}: ConfirmationStepProps) { const {isOffline} = useNetwork(); const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: false}); - const firstAssigneeEmail = useInitial(assignCard?.data?.email); - const shouldUseBackToParam = !firstAssigneeEmail || firstAssigneeEmail === assignCard?.data?.email; + const firstAssigneeEmail = useInitial(assignCard?.cardToAssign?.email); + const shouldUseBackToParam = !firstAssigneeEmail || firstAssigneeEmail === assignCard?.cardToAssign?.email; const backTo = shouldUseBackToParam ? route.params?.backTo : undefined; const policy = usePolicy(policyID); const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); const [currencyList = getEmptyObject()] = useOnyx(ONYXKEYS.CURRENCY_LIST, {canBeMissing: true}); - const bankName = assignCard?.data?.bankName ?? getCompanyCardFeed(feed); + const bankName = assignCard?.cardToAssign?.bankName ?? getCompanyCardFeed(feed); const [cardFeeds] = useCardFeeds(policyID); const companyCardFeedData = cardFeeds?.[feed]; @@ -57,7 +57,7 @@ function ConfirmationStep({route}: ConfirmationStepProps) { const workspaceAccountID = useWorkspaceAccountID(policyID); const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyCardFeedData); - const data = assignCard?.data; + const data = assignCard?.cardToAssign; const cardholder = getPersonalDetailByEmail(data?.email ?? ''); const cardholderName = Str.removeSMSDomain(cardholder?.displayName ?? ''); diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx index 47a2fdbb8dedd..4766915ad031a 100644 --- a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -39,8 +39,8 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb clearInviteDraft(policyID); setAssignCardStepAndData({ currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE, - data: { - ...assignCard?.data, + cardToAssign: { + ...assignCard?.cardToAssign, invitingMemberEmail: undefined, invitingMemberAccountID: undefined, }, @@ -50,55 +50,55 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb }; const goToNextStep = useCallback(() => { - const data: Partial = { - email: assignCard?.data?.invitingMemberEmail, - cardName: getDefaultCardName(assignCard?.data?.invitingMemberEmail), + const cardToAssign: Partial = { + email: assignCard?.cardToAssign?.invitingMemberEmail, + cardName: getDefaultCardName(assignCard?.cardToAssign?.invitingMemberEmail), invitingMemberEmail: '', }; const routeParams = {policyID, feed, cardID}; - if (assignCard?.data?.encryptedCardNumber) { - data.encryptedCardNumber = assignCard.data.encryptedCardNumber; - data.cardNumber = assignCard.data.cardNumber; - data.startDate = assignCard?.data?.startDate ?? new Date().toISOString().split('T').at(0); - data.dateOption = assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + if (assignCard?.cardToAssign?.encryptedCardNumber) { + cardToAssign.encryptedCardNumber = assignCard.cardToAssign.encryptedCardNumber; + cardToAssign.cardNumber = assignCard.cardToAssign.cardNumber; + cardToAssign.startDate = assignCard?.cardToAssign?.startDate ?? new Date().toISOString().split('T').at(0); + cardToAssign.dateOption = assignCard?.cardToAssign?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; setAssignCardStepAndData({ currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, - data, + cardToAssign, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION.getRoute(routeParams)); } else if (hasOnlyOneCardToAssign(filteredCardList)) { - data.cardNumber = Object.keys(filteredCardList).at(0); - data.encryptedCardNumber = Object.values(filteredCardList).at(0); - data.startDate = assignCard?.data?.startDate ?? new Date().toISOString().split('T').at(0); - data.dateOption = assignCard?.data?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; + cardToAssign.cardNumber = Object.keys(filteredCardList).at(0); + cardToAssign.encryptedCardNumber = Object.values(filteredCardList).at(0); + cardToAssign.startDate = assignCard?.cardToAssign?.startDate ?? new Date().toISOString().split('T').at(0); + cardToAssign.dateOption = assignCard?.cardToAssign?.dateOption ?? CONST.COMPANY_CARD.TRANSACTION_START_DATE_OPTIONS.CUSTOM; setAssignCardStepAndData({ currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, - data, + cardToAssign, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CONFIRMATION.getRoute(routeParams)); } else { setAssignCardStepAndData({ currentStep: CONST.COMPANY_CARD.STEP.CARD, - data, + cardToAssign, isEditing: false, }); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD_CARD_SELECTION.getRoute(routeParams)); } - }, [assignCard?.data, filteredCardList, policyID, feed, cardID]); + }, [assignCard?.cardToAssign, filteredCardList, policyID, feed, cardID]); // If the currently inviting member is already a member of the policy then we should just call goToNextStep // See https://github.com/Expensify/App/issues/74256 for more details useEffect(() => { - setDraftInviteAccountID(assignCard?.data?.invitingMemberEmail ?? '', assignCard?.data?.invitingMemberAccountID ?? undefined, policyID); - if (!policy?.employeeList?.[assignCard?.data?.invitingMemberEmail ?? '']) { + setDraftInviteAccountID(assignCard?.cardToAssign?.invitingMemberEmail ?? '', assignCard?.cardToAssign?.invitingMemberAccountID ?? undefined, policyID); + if (!policy?.employeeList?.[assignCard?.cardToAssign?.invitingMemberEmail ?? '']) { return; } goToNextStep(); - }, [assignCard?.data?.invitingMemberEmail, policy?.employeeList, goToNextStep, assignCard?.data?.invitingMemberAccountID, policyID]); + }, [assignCard?.cardToAssign?.invitingMemberEmail, policy?.employeeList, goToNextStep, assignCard?.cardToAssign?.invitingMemberAccountID, policyID]); return ( diff --git a/src/types/onyx/AssignCard.ts b/src/types/onyx/AssignCard.ts index b353d4c007f2e..ad3d0fd3636c9 100644 --- a/src/types/onyx/AssignCard.ts +++ b/src/types/onyx/AssignCard.ts @@ -59,7 +59,7 @@ type AssignCard = { currentStep: AssignCardStep; /** Data required to be sent to assign a card */ - data: Partial; + cardToAssign: Partial; /** Whether the user is editing step */ isEditing: boolean; diff --git a/tests/ui/AssignCardFeedPage.tsx b/tests/ui/AssignCardFeedPage.tsx index 3ec6ca95ca0b7..045a4d5b594aa 100644 --- a/tests/ui/AssignCardFeedPage.tsx +++ b/tests/ui/AssignCardFeedPage.tsx @@ -132,7 +132,7 @@ describe('AssignCardFeedPage', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); await Onyx.merge(ONYXKEYS.ASSIGN_CARD, { - data: { + cardToAssign: { bankName: 'vcf', email: 'testaccount+1@gmail.com', cardName: "Test 1's card", @@ -200,7 +200,7 @@ describe('AssignCardFeedPage', () => { await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); await Onyx.merge(ONYXKEYS.ASSIGN_CARD, { - data: { + cardToAssign: { bankName: 'vcf', email: 'testaccount+1@gmail.com', cardName: "Test 1's card", @@ -227,7 +227,7 @@ describe('AssignCardFeedPage', () => { // Mock the action of changing the assignee of the card await act(async () => { await Onyx.merge(ONYXKEYS.ASSIGN_CARD, { - data: { + cardToAssign: { email: 'testaccount+2@gmail.com', }, }); From d29a28cf23e2a2836e244c80270cc91e70c7b95b Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:25:56 -0500 Subject: [PATCH 25/36] fix: prettier --- src/components/Table/TableBody.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index c7f5d7389126a..02fd25032776c 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -82,7 +82,10 @@ function TableBody({contentContainerStyle, ...props}: TableBodyProps) { return ( // eslint-disable-next-line react/jsx-props-no-spreading - + data={filteredAndSortedData} ListEmptyComponent={isEmptyResult ? EmptyResultComponent : ListEmptyComponent} From 2e4b76920349e8e89577d956ad9bdf855221b013 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:38:47 -0500 Subject: [PATCH 26/36] fix: feed route param types --- src/components/Table/TableBody.tsx | 2 +- src/libs/Navigation/types.ts | 10 +++++----- .../companyCards/BankConnection/index.native.tsx | 2 +- .../companyCards/BankConnection/index.tsx | 2 +- .../WorkspaceCompanyCardAccountSelectCardPage.tsx | 15 +++++++-------- .../WorkspaceCompanyCardDetailsPage.tsx | 2 +- .../WorkspaceCompanyCardEditCardNamePage.tsx | 11 +++++------ .../WorkspaceCompanyCardsTableItem.tsx | 10 +++------- 8 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 02fd25032776c..893c6624107f4 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -81,9 +81,9 @@ function TableBody({contentContainerStyle, ...props}: TableBodyProps) { ); return ( - // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 60dec4f299165..c6a7190da5e87 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -24,7 +24,7 @@ import type {Country, IOUAction, IOUType} from '@src/CONST'; import type NAVIGATORS from '@src/NAVIGATORS'; import type {Route as ExpensifyRoute, Route as Routes} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {ConnectionName, SageIntacctMappingName} from '@src/types/onyx/Policy'; import type {CustomFieldType} from '@src/types/onyx/PolicyEmployee'; import type {FileObject} from '@src/types/utils/Attachment'; @@ -1149,13 +1149,13 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: { policyID: string; - bankName: CompanyCardFeedWithDomainID; + feed: CompanyCardFeedWithDomainID; // 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; }; [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: { policyID: string; - bank: CompanyCardFeed; + feed: CompanyCardFeedWithDomainID; 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; @@ -1163,12 +1163,12 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARD_NAME]: { policyID: string; cardID: string; - bank: string; + feed: CompanyCardFeedWithDomainID; }; [SCREENS.WORKSPACE.COMPANY_CARD_EXPORT]: { policyID: string; cardID: string; - bank: string; + feed: CompanyCardFeedWithDomainID; // 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/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 9825abac1eb9c..9a6688cb43cb8 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -51,7 +51,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti const authToken = session?.authToken ?? null; const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); const selectedBank = addNewCard?.data?.selectedBank; - const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; + const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); const {isBetaEnabled} = usePermissions(); diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index e911a95e46b42..3e784ba9130ce 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -51,7 +51,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti const {translate} = useLocalize(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); - const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; + const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; const [cardFeeds] = useCardFeeds(policyID); const prevFeedsData = usePrevious(cardFeeds); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx index fcfa3cdd79dc4..8c1372ab26238 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardAccountSelectCardPage.tsx @@ -24,7 +24,6 @@ import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import {getExportMenuItem} from './utils'; type WorkspaceCompanyCardAccountSelectCardProps = PlatformStackScreenProps; @@ -34,12 +33,12 @@ function WorkspaceCompanyCardAccountSelectCardPage({route}: WorkspaceCompanyCard const styles = useThemeStyles(); const {environmentURL} = useEnvironment(); const {policyID, cardID, backTo} = route.params; - const bank = decodeURIComponent(route.params.bank) as CompanyCardFeedWithDomainID; + const feed = route.params.feed; const policy = usePolicy(policyID); const workspaceAccountID = useWorkspaceAccountID(policyID); const [searchText, setSearchText] = useState(''); - const [allBankCards] = useCardsList(bank); + const [allBankCards] = useCardsList(feed); const card = allBankCards?.[cardID]; const connectedIntegration = getConnectedIntegration(policy) ?? CONST.POLICY.CONNECTIONS.NAME.QBO; // We need to have an unchanged active route for getExportMenuItem so the export page link is not updated incorrectly when user is in other pages @@ -54,7 +53,7 @@ function WorkspaceCompanyCardAccountSelectCardPage({route}: WorkspaceCompanyCard const [cardFeeds] = useCardFeeds(policyID); const companyFeeds = getCompanyFeeds(cardFeeds); - const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[bank]); + const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feed]); const searchedListOptions = useMemo(() => { return tokenizedSearch(exportMenuItem?.data ?? [], searchText, (option) => [option.value]); @@ -81,11 +80,11 @@ function WorkspaceCompanyCardAccountSelectCardPage({route}: WorkspaceCompanyCard } const isDefaultCardSelected = value === defaultCard; const exportValue = isDefaultCardSelected ? CONST.COMPANY_CARDS.DEFAULT_EXPORT_TYPE : value; - setCompanyCardExportAccount(policyID, domainOrWorkspaceAccountID, cardID, exportMenuItem.exportType, exportValue, getCompanyCardFeed(bank)); + setCompanyCardExportAccount(policyID, domainOrWorkspaceAccountID, cardID, exportMenuItem.exportType, exportValue, getCompanyCardFeed(feed)); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, bank)); + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, feed)); }, - [exportMenuItem?.exportType, domainOrWorkspaceAccountID, cardID, policyID, bank, defaultCard], + [exportMenuItem?.exportType, domainOrWorkspaceAccountID, cardID, policyID, feed, defaultCard], ); return ( @@ -118,7 +117,7 @@ function WorkspaceCompanyCardAccountSelectCardPage({route}: WorkspaceCompanyCard onChangeText={setSearchText} onSelectRow={updateExportAccount} initiallyFocusedOptionKey={exportMenuItem?.data?.find((mode) => mode.isSelected)?.keyForList} - onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, bank, backTo), {compareParams: false})} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, feed, backTo), {compareParams: false})} headerTitleAlreadyTranslated={exportMenuItem?.description} listEmptyContent={listEmptyContent} connectionName={connectedIntegration} diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx index f184ff8dbf192..cadff4938fe3b 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx @@ -49,7 +49,7 @@ type WorkspaceCompanyCardDetailsPageProps = PlatformStackScreenProps; @@ -33,7 +32,7 @@ type WorkspaceCompanyCardEditCardNamePageProps = PlatformStackScreenProps) => { - updateCompanyCardName(domainOrWorkspaceAccountID, cardID, values[INPUT_IDS.NAME], getCompanyCardFeed(bank), defaultValue); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, bank), {compareParams: false}); + updateCompanyCardName(domainOrWorkspaceAccountID, cardID, values[INPUT_IDS.NAME], getCompanyCardFeed(feed), defaultValue); + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, feed), {compareParams: false}); }; const validate = (values: FormOnyxValues): FormInputErrors => { @@ -82,7 +81,7 @@ function WorkspaceCompanyCardEditCardNamePage({route}: WorkspaceCompanyCardEditC > Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, bank), {compareParams: false})} + onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.getRoute(policyID, cardID, feed), {compareParams: false})} /> {translate('workspace.moreFeatures.companyCards.giveItNameInstruction')} {({hovered}) => ( From 00c472474f38e6fe49ecafdbe62e4476fcaeb488 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:41:45 -0500 Subject: [PATCH 27/36] fix: load members on company cards page open --- src/pages/workspace/WorkspaceMembersPage.tsx | 3 ++- .../workspace/companyCards/WorkspaceCompanyCardsPage.tsx | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 3bc5491e370d7..7b4375fd9f712 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -173,7 +173,8 @@ function WorkspaceMembersPage({personalDetails, route, policy}: WorkspaceMembers * Get members for the current workspace */ const getWorkspaceMembers = useCallback(() => { - openWorkspaceMembersPage(route.params.policyID, Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList))); + const clientMemberEmails = Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList)); + openWorkspaceMembersPage(route.params.policyID, clientMemberEmails); }, [route.params.policyID, policy?.employeeList]); useEffect(() => { diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 99de3c33c569c..e94834b449412 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -12,9 +12,11 @@ import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {openWorkspaceMembersPage} from '@libs/actions/Policy/Member'; import {getCompanyCardFeed, getCompanyFeeds, getDomainOrWorkspaceAccountID, getSelectedFeed} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; +import {getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import variables from '@styles/variables'; @@ -88,8 +90,10 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) { return; } + const clientMemberEmails = Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList)); + openWorkspaceMembersPage(policyID, clientMemberEmails); openPolicyCompanyCardsFeed(domainOrWorkspaceAccountID, policyID, feed); - }, [feed, isLoading, policyID, isFeedPending, domainOrWorkspaceAccountID]); + }, [feed, isLoading, policyID, isFeedPending, domainOrWorkspaceAccountID, policy?.employeeList]); const {assignCard, isAssigningCardDisabled} = useAssignCard({selectedFeed, policyID, setShouldShowOfflineModal}); From 3126a53d0cac020b14cb0d4af905e9674adaf129 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:54:40 -0500 Subject: [PATCH 28/36] fix: remove reference of old "New card" button on member spage --- .../ModalStackNavigators/index.tsx | 1 - .../RELATIONS/WORKSPACE_TO_RHP.ts | 2 - src/libs/Navigation/linkingConfig/config.ts | 3 - src/libs/Navigation/types.ts | 4 - .../assignCard/CardSelectionStep.tsx | 1 - .../members/WorkspaceMemberNewCardPage.tsx | 228 ------------------ src/pages/workspace/withPolicy.tsx | 1 - 7 files changed, 240 deletions(-) delete mode 100644 src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ebdd6f9f58744..9580d6f658592 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -433,7 +433,6 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/members/WorkspaceMemberDetailsPage').default, [SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE]: () => require('../../../../pages/workspace/members/WorkspaceMemberDetailsRolePage').default, [SCREENS.WORKSPACE.MEMBER_CUSTOM_FIELD]: () => require('../../../../pages/workspace/members/WorkspaceMemberCustomFieldPage').default, - [SCREENS.WORKSPACE.MEMBER_NEW_CARD]: () => require('../../../../pages/workspace/members/WorkspaceMemberNewCardPage').default, [SCREENS.WORKSPACE.OWNER_CHANGE_CHECK]: () => require('@pages/workspace/members/WorkspaceOwnerChangeWrapperPage').default, [SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS]: () => require('../../../../pages/workspace/members/WorkspaceOwnerChangeSuccessPage').default, [SCREENS.WORKSPACE.OWNER_CHANGE_ERROR]: () => require('../../../../pages/workspace/members/WorkspaceOwnerChangeErrorPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 8da85b46b3904..9f810adc5f269 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -17,7 +17,6 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.MEMBER_CUSTOM_FIELD]: { path: ROUTES.WORKSPACE_CUSTOM_FIELDS.route, }, - [SCREENS.WORKSPACE.MEMBER_NEW_CARD]: { - path: ROUTES.WORKSPACE_MEMBER_NEW_CARD.route, - }, [SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS]: { path: ROUTES.WORKSPACE_OWNER_CHANGE_SUCCESS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c6a7190da5e87..9f9adfe8bf1f9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -596,10 +596,6 @@ type SettingsNavigatorParamList = { accountID: string; customFieldType: CustomFieldType; }; - [SCREENS.WORKSPACE.MEMBER_NEW_CARD]: { - policyID: string; - accountID: string; - }; [SCREENS.WORKSPACE.OWNER_CHANGE_SUCCESS]: { policyID: string; accountID: number; diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx index c5e0fe4fe982e..829ba510b324b 100644 --- a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -31,7 +31,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; type CardSelectionStepProps = PlatformStackScreenProps; diff --git a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx b/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx deleted file mode 100644 index 0191328478dda..0000000000000 --- a/src/pages/workspace/members/WorkspaceMemberNewCardPage.tsx +++ /dev/null @@ -1,228 +0,0 @@ -import React, {useState} from 'react'; -import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import Icon from '@components/Icon'; -import PlaidCardFeedIcon from '@components/PlaidCardFeedIcon'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import RadioListItem from '@components/SelectionList/ListItem/RadioListItem'; -import type {ListItem} from '@components/SelectionList/types'; -import type {CombinedCardFeed} from '@hooks/useCardFeeds'; -import useCardFeeds from '@hooks/useCardFeeds'; -import useCardsList from '@hooks/useCardsList'; -import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; -import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; -import useThemeIllustrations from '@hooks/useThemeIllustrations'; -import useThemeStyles from '@hooks/useThemeStyles'; -import { - getCardFeedIcon, - getCompanyCardFeed, - getCompanyFeeds, - getCustomOrFormattedFeedName, - getDomainOrWorkspaceAccountID, - getFilteredCardList, - getPlaidInstitutionIconUrl, - hasOnlyOneCardToAssign, - isCustomFeed, - isExpensifyCardFullySetUp, - isSelectedFeedExpired, -} from '@libs/CardUtils'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import Navigation from '@navigation/Navigation'; -import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; -import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; -import variables from '@styles/variables'; -import {setIssueNewCardStepAndData} from '@userActions/Card'; -import {openAssignFeedCardPage, setAssignCardStepAndData} from '@userActions/CompanyCards'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import type {CompanyCardFeed, CompanyCardFeedWithDomainID} from '@src/types/onyx'; -import type {AssignCardData, AssignCardStep} from '@src/types/onyx/AssignCard'; - -type CardFeedListItem = ListItem & { - /** Combined feed key */ - value: CompanyCardFeedWithDomainID; - - /** Card feed value */ - feed: CompanyCardFeed; -}; - -type WorkspaceMemberNewCardPageProps = WithPolicyAndFullscreenLoadingProps & PlatformStackScreenProps; - -function WorkspaceMemberNewCardPage({route, personalDetails}: WorkspaceMemberNewCardPageProps) { - const {policyID} = route.params; - const policy = usePolicy(policyID); - const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; - - const {translate} = useLocalize(); - const styles = useThemeStyles(); - const lazyIllustrations = useMemoizedLazyIllustrations(['ExpensifyCardImage']); - const illustrations = useThemeIllustrations(); - const companyCardFeedIcons = useCompanyCardFeedIcons(); - const [cardFeeds, , defaultFeed] = useCardFeeds(policyID); - const [selectedFeed, setSelectedFeed] = useState(''); - const [shouldShowError, setShouldShowError] = useState(false); - const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, {canBeMissing: true}); - const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST, {canBeMissing: true}); - - const accountID = Number(route.params.accountID); - const memberLogin = personalDetails?.[accountID]?.login ?? ''; - const memberName = personalDetails?.[accountID]?.firstName ? personalDetails?.[accountID]?.firstName : personalDetails?.[accountID]?.login; - const companyFeeds = getCompanyFeeds(cardFeeds, false, true); - const currentFeed = selectedFeed ? cardFeeds?.[selectedFeed as CompanyCardFeedWithDomainID] : undefined; - const isFeedExpired = isSelectedFeedExpired(currentFeed); - const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, currentFeed); - - const [list] = useCardsList(selectedFeed as CompanyCardFeedWithDomainID); - const filteredCardList = getFilteredCardList(list, currentFeed?.accountList, workspaceCardFeeds); - - const shouldShowExpensifyCard = isExpensifyCardFullySetUp(policy, cardSettings); - - const handleSubmit = () => { - if (!selectedFeed) { - setShouldShowError(true); - return; - } - if (selectedFeed === CONST.EXPENSIFY_CARD.NAME) { - setIssueNewCardStepAndData({ - step: CONST.EXPENSIFY_CARD.STEP.CARD_TYPE, - data: { - assigneeEmail: memberLogin, - }, - isEditing: false, - isChangeAssigneeDisabled: true, - policyID, - }); - Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW.getRoute(policyID, ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID))); - } else { - const data: Partial = { - email: memberLogin, - bankName: getCompanyCardFeed(selectedFeed as CompanyCardFeedWithDomainID), - cardName: `${memberName}'s card`, - }; - let currentStep: AssignCardStep = CONST.COMPANY_CARD.STEP.CARD; - - if (hasOnlyOneCardToAssign(filteredCardList)) { - currentStep = CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE; - data.cardNumber = Object.keys(filteredCardList).at(0); - data.encryptedCardNumber = Object.values(filteredCardList).at(0); - } - if (isFeedExpired) { - currentStep = CONST.COMPANY_CARD.STEP.BANK_CONNECTION; - } - setAssignCardStepAndData({ - currentStep, - cardToAssign: data, - isEditing: false, - }); - Navigation.setNavigationActionToMicrotaskQueue(() => - Navigation.navigate( - ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.getRoute({policyID, feed: selectedFeed, cardID: undefined}, ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, accountID)), - ), - ); - } - }; - - const handleSelectFeed = ({value, feed}: CardFeedListItem) => { - setSelectedFeed(value); - const workspaceCards = workspaceCardFeeds?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${feed}`] ?? {}; - const hasAllCardsData = !!workspaceCards.cardList; - if (isCustomFeed(feed) && !hasAllCardsData) { - openAssignFeedCardPage(policyID, feed, domainOrWorkspaceAccountID); - } - setShouldShowError(false); - }; - - const companyCardFeeds: CardFeedListItem[] = (Object.entries(companyFeeds) as Array<[CompanyCardFeedWithDomainID, CombinedCardFeed]>).map(([key, value]) => { - const plaidUrl = getPlaidInstitutionIconUrl(value.feed); - - return { - value: key, - feed: value.feed, - text: getCustomOrFormattedFeedName(value.feed, value.customFeedName), - keyForList: key, - isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - pendingAction: value.pendingAction, - isSelected: selectedFeed === key, - - leftElement: plaidUrl ? ( - - ) : ( - - ), - }; - }); - - const feeds = shouldShowExpensifyCard - ? [ - ...companyCardFeeds, - { - value: CONST.EXPENSIFY_CARD.NAME as CompanyCardFeedWithDomainID, - feed: CONST.EXPENSIFY_CARD.NAME as CompanyCardFeed, - text: translate('workspace.common.expensifyCard'), - keyForList: CONST.EXPENSIFY_CARD.NAME, - isSelected: selectedFeed === CONST.EXPENSIFY_CARD.NAME, - leftElement: ( - - ), - }, - ] - : companyCardFeeds; - - const goBack = () => Navigation.goBack(); - - return ( - - - - - - - - ); -} - -export default withPolicyAndFullscreenLoading(WorkspaceMemberNewCardPage); diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index 1245cce7ef6b3..d7ac368ecb9cf 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -34,7 +34,6 @@ type PolicyRouteName = | typeof SCREENS.WORKSPACE.MEMBER_DETAILS | typeof SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE | typeof SCREENS.WORKSPACE.MEMBER_CUSTOM_FIELD - | typeof SCREENS.WORKSPACE.MEMBER_NEW_CARD | typeof SCREENS.WORKSPACE.INVOICES | typeof SCREENS.WORKSPACE.OWNER_CHANGE_CHECK | typeof SCREENS.WORKSPACE.TAX_EDIT From 6be5734a420b315bda6f224747efa3a0cc9091ea Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 12:56:03 -0500 Subject: [PATCH 29/36] fix: TS error --- .../workspace/companyCards/assignCard/InviteNewMemberStep.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx index 4766915ad031a..55bb34cf8c6e7 100644 --- a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -30,7 +30,7 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb const policyID = route.params.policyID; const feed = route.params.feed; const cardID = route.params.cardID; - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false}); const [list] = useCardsList(feed); const [cardFeeds] = useCardFeeds(policy?.id); const filteredCardList = getFilteredCardList(list, cardFeeds?.[feed]?.accountList, workspaceCardFeeds); From 6616e5291ef49a347f58fcd6e1f0733437502c68 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 13:28:12 -0500 Subject: [PATCH 30/36] fix: more lint errors --- src/libs/actions/CompanyCards.ts | 4 ++-- .../workspace/companyCards/assignCard/CardNameStep.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 7376d7822e786..9b24091b0db07 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -47,8 +47,8 @@ type AddNewCompanyCardFlowData = { data?: Partial; }; -function setAssignCardStepAndData({cardToAssign: cardToAssign, isEditing, currentStep}: Partial) { - Onyx.merge(ONYXKEYS.ASSIGN_CARD, {cardToAssign: cardToAssign, isEditing, currentStep}); +function setAssignCardStepAndData({cardToAssign, isEditing, currentStep}: Partial) { + Onyx.merge(ONYXKEYS.ASSIGN_CARD, {cardToAssign, isEditing, currentStep}); } function setTransactionStartDate(startDate: string) { diff --git a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx index c37cd09f5ebbd..9a7556637a429 100644 --- a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx @@ -32,7 +32,7 @@ function CardNameStep({route}: CardNameStepProps) { const policyID = route.params.policyID; - const data = assignCard?.cardToAssign; + const cardToAssign = assignCard?.cardToAssign; const submit = (values: FormOnyxValues) => { setAssignCardStepAndData({ @@ -74,7 +74,7 @@ function CardNameStep({route}: CardNameStepProps) { /> {translate('workspace.moreFeatures.companyCards.giveItNameInstruction')} From 2f9eb114d6193a9b4b1e174097a8abd9491aae22 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 13:40:29 -0500 Subject: [PATCH 31/36] fix: `filterInactiveCards` test failures --- src/libs/CardUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 93293be77bc08..af7848b9ff2a8 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -638,7 +638,7 @@ function checkIfNewFeedConnected(prevFeedsData: CompanyFeeds, currentFeedsData: } function filterInactiveCards(cardsList: WorkspaceCardsList | undefined) { - const {cardList = {}, ...assignedCards} = cardsList ?? {}; + const {cardList, ...assignedCards} = cardsList ?? {}; const closedStates = new Set([CONST.EXPENSIFY_CARD.STATE.CLOSED, CONST.EXPENSIFY_CARD.STATE.STATE_DEACTIVATED, CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED]); const filteredAssignedCards = filterObject(assignedCards, (_key, card) => !closedStates.has(card.state)); From de0912533bce8cd49dda50cc9a46ea5c208fe413 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 13:46:16 -0500 Subject: [PATCH 32/36] fix: AssignCardFeed ui tests --- .../{AssignCardFeedPage.tsx => AssignCardFeed.tsx} | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) rename tests/ui/{AssignCardFeedPage.tsx => AssignCardFeed.tsx} (97%) diff --git a/tests/ui/AssignCardFeedPage.tsx b/tests/ui/AssignCardFeed.tsx similarity index 97% rename from tests/ui/AssignCardFeedPage.tsx rename to tests/ui/AssignCardFeed.tsx index 045a4d5b594aa..78b89f03905df 100644 --- a/tests/ui/AssignCardFeedPage.tsx +++ b/tests/ui/AssignCardFeed.tsx @@ -21,6 +21,10 @@ import * as LHNTestUtils from '../utils/LHNTestUtils'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +const FEED_DOMAIN_ID = 1234; +const FEED = `${CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX}.${FEED_DOMAIN_ID}`; +const CARD_ID = '1234'; + // Set up a global fetch mock for API requests in tests. TestHelper.setupGlobalFetchMock(); @@ -92,7 +96,7 @@ const renderPage = ( ); }; -describe('AssignCardFeedPage', () => { +describe('AssignCardFeed', () => { beforeAll(() => { // Initialize Onyx with required keys before running any test. Onyx.init({ @@ -150,8 +154,8 @@ describe('AssignCardFeedPage', () => { // Render the page with the specified policyID and backTo param const {unmount} = renderPage(SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE, { policyID: policy.id, - feed: CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, - cardID: '1234', + feed: FEED, + cardID: CARD_ID, backTo: ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policy?.id, 1234), }); @@ -217,8 +221,8 @@ describe('AssignCardFeedPage', () => { // Render the page with the specified policyID and backTo param const {unmount} = renderPage(SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE, { policyID: policy.id, - feed: CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX, - cardID: '1234', + feed: FEED, + cardID: CARD_ID, backTo: ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policy?.id, 1234), }); From 9d6ae23b5445ae24871ef4a87a68f45f4844da32 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 13:48:37 -0500 Subject: [PATCH 33/36] revert: invalid changes to snapshot type --- src/libs/actions/IOU.ts | 16 ++++++++-------- src/libs/actions/Report.ts | 4 ++-- src/libs/actions/Search.ts | 14 +++++++------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 719398959365e..c6095be3feeba 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -4897,7 +4897,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]: violationsOnyxData.value, }, }, @@ -4907,7 +4907,7 @@ function getUpdateMoneyRequestParams(params: GetUpdateMoneyRequestParamsType): U onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`]: currentTransactionViolations, }, }, @@ -9571,7 +9571,7 @@ function deleteMoneyRequest({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`]: { // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 pendingFields: { @@ -12363,7 +12363,7 @@ function replaceReceipt({transactionID, file, source, transactionPolicy, transac onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { receipt: receiptOptimistic, }, @@ -12376,7 +12376,7 @@ function replaceReceipt({transactionID, file, source, transactionPolicy, transac onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: { receipt: !isEmptyObject(oldReceipt) ? oldReceipt : null, }, @@ -13503,7 +13503,7 @@ function getSearchOnyxUpdate({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, value: { - cardToAssign: { + data: { [ONYXKEYS.PERSONAL_DETAILS_LIST]: { [toAccountID]: { accountID: toAccountID, @@ -15046,7 +15046,7 @@ function updateSplitTransactions({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContext?.currentSearchHash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: null, }, }, @@ -15057,7 +15057,7 @@ function updateSplitTransactions({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${searchContext?.currentSearchHash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${originalTransactionID}`]: originalTransaction, }, }, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 163f072417867..baa920755cbdc 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -2967,7 +2967,7 @@ function buildNewReportOptimisticData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, value: { - cardToAssign: {[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: optimisticReportData}, + data: {[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: optimisticReportData}, }, }); } @@ -6000,7 +6000,7 @@ function buildOptimisticChangePolicyData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${currentSearchQueryJSON.hash}` as const, value: { - cardToAssign: {[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: policy}, + data: {[`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`]: policy}, }, }); } diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 91f735f60729a..9452647647427 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -255,7 +255,7 @@ function getOnyxLoadingData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - ...(isOffline ? {} : {cardToAssign: []}), + ...(isOffline ? {} : {data: []}), search: { status: queryJSON?.status, type: queryJSON?.type, @@ -468,7 +468,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: Report[], policy: onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, null])), + data: Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`, null])), }, }); } @@ -524,7 +524,7 @@ function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], curre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), + data: Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null])), }, }); } @@ -589,7 +589,7 @@ function exportToIntegrationOnSearch(hash: number, reportID: string, connectionN onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]: null, }, }, @@ -651,7 +651,7 @@ function payMoneyRequestOnSearch(hash: number, paymentData: PaymentData[], curre onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])), + data: Object.fromEntries(paymentData.map((item) => [`${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, null])), }, }); } @@ -702,7 +702,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: { + data: { [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, }, }, @@ -714,7 +714,7 @@ function deleteMoneyRequestOnSearch(hash: number, transactionIDList: string[]) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, value: { - cardToAssign: { + data: { // @ts-expect-error - will be solved in https://github.com/Expensify/App/issues/73830 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]: {pendingAction: null}, From 912880ce0dce943597c384542f913af530abd8b6 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 16:34:59 -0500 Subject: [PATCH 34/36] fix: hide popover label --- src/components/Search/FilterDropdowns/SingleSelectPopup.tsx | 6 ++++-- .../Table/TableFilterButtons/buildFilterItems.tsx | 2 +- src/components/Table/middlewares/filtering.ts | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx b/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx index 00d4d3ffac87a..4ad687e40ef03 100644 --- a/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx +++ b/src/components/Search/FilterDropdowns/SingleSelectPopup.tsx @@ -18,7 +18,7 @@ type SingleSelectItem = { type SingleSelectPopupProps = { /** The label to show when in an overlay on mobile */ - label: string; + label?: string; /** The list of all items to show up in the list */ items: Array>; @@ -107,9 +107,11 @@ function SingleSelectPopup({label, value, items, closeOverlay, [searchTerm, isSearchable, searchPlaceholder, translate, setSearchTerm, noResultsFound], ); + const shouldShowLabel = isSmallScreenWidth && !!label; + return ( - {isSmallScreenWidth && {label}} + {shouldShowLabel && {label}} ({filterKey return ( ({ text: option.label, value: option.value, diff --git a/src/components/Table/middlewares/filtering.ts b/src/components/Table/middlewares/filtering.ts index e70c450301ebc..ab01b56fed06f 100644 --- a/src/components/Table/middlewares/filtering.ts +++ b/src/components/Table/middlewares/filtering.ts @@ -7,6 +7,7 @@ import type {Middleware, MiddlewareHookResult} from './types'; * @template FilterKey - The type of filter keys. */ type FilterConfigEntry = { + showLabel?: boolean; filterType?: 'multi-select' | 'single-select'; options: Array<{label: string; value: string}>; default?: string; From 20edc5210993c0f4ae1ed11691557ac0ef3ee941 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 16:44:36 -0500 Subject: [PATCH 35/36] fix: TS errors --- src/components/Table/TableHeader.tsx | 3 ++- src/libs/CardUtils.ts | 2 +- .../settings/Profile/Contacts/ContactMethodDetailsPage.tsx | 1 - tests/ui/AssignCardFeed.tsx | 7 ++++--- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Table/TableHeader.tsx b/src/components/Table/TableHeader.tsx index 09200d284d595..c4154cbe9466b 100644 --- a/src/components/Table/TableHeader.tsx +++ b/src/components/Table/TableHeader.tsx @@ -120,7 +120,7 @@ function TableHeaderColumn({column}: {colu accessible accessibilityLabel={column.label} accessibilityRole="button" - style={[column.styling?.labelStyles, styles.flexRow, styles.alignItemsCenter, column.styling?.flex ? {flex: column.styling.flex} : styles.flex1, column.styling?.containerStyles]} + style={[styles.flexRow, styles.alignItemsCenter, column.styling?.flex ? {flex: column.styling.flex} : styles.flex1, column.styling?.containerStyles]} onPress={() => toggleSorting(column.key)} > ({column}: {colu style={[ styles.lh16, isSortingByColumn ? styles.textMicroBoldSupporting : [styles.textMicroSupporting, styles.pr1, {marginRight: variables.iconSizeExtraSmall, marginBottom: 1, marginTop: 1}], + column.styling?.labelStyles, ]} > {column.label} diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index af7848b9ff2a8..de87574b7fdf5 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -644,7 +644,7 @@ function filterInactiveCards(cardsList: WorkspaceCardsList | undefined) { const filteredAssignedCards = filterObject(assignedCards, (_key, card) => !closedStates.has(card.state)); return { - cardList, + ...(cardList ? {cardList} : {}), ...filteredAssignedCards, } as WorkspaceCardsList; } diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx index 272879da8234b..1ae012ef24520 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.tsx @@ -325,7 +325,6 @@ function ContactMethodDetailsPage({route}: ContactMethodDetailsPageProps) { clearUnvalidatedNewContactMethodAction(); Navigation.goBack(ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)); }} - canDismissError /> )} {isValidateCodeFormVisible && !!loginData && !loginData.validatedDate && ( diff --git a/tests/ui/AssignCardFeed.tsx b/tests/ui/AssignCardFeed.tsx index 78b89f03905df..aad55dc93ed60 100644 --- a/tests/ui/AssignCardFeed.tsx +++ b/tests/ui/AssignCardFeed.tsx @@ -12,17 +12,18 @@ import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types'; import Navigation from '@libs/Navigation/Navigation'; import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import AssignCardFeedPage from '@pages/workspace/companyCards/BrokenCardFeedConnectionPage'; +import AssignCardFeedAssigneePage from '@pages/workspace/companyCards/assignCard/AssigneeStep'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx/CardFeeds'; import * as LHNTestUtils from '../utils/LHNTestUtils'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; const FEED_DOMAIN_ID = 1234; -const FEED = `${CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX}.${FEED_DOMAIN_ID}`; +const FEED = `${CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX}.${FEED_DOMAIN_ID}` as CompanyCardFeedWithDomainID; const CARD_ID = '1234'; // Set up a global fetch mock for API requests in tests. @@ -86,7 +87,7 @@ const renderPage = ( From bad449ec94f78dfff80c7474be4280eb69c30d49 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Fri, 19 Dec 2025 17:00:03 -0500 Subject: [PATCH 36/36] fix: table header label styles --- .../workspace/companyCards/WorkspaceCompanyCardsTable.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx index 3e0dedf742734..4f6c825714703 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsTable.tsx @@ -96,8 +96,7 @@ function WorkspaceCompanyCardsTable({ key: 'customCardName', label: translate('workspace.companyCards.cardName'), styling: { - containerStyles: [styles.justifyContentEnd], - labelStyles: [styles.pr3], + containerStyles: [styles.justifyContentEnd, styles.pr3], }, }, ];