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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure",
"typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc",
"typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo",
"lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=313 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto",
"lint": "NODE_OPTIONS=--max_old_space_size=8192 eslint . --max-warnings=292 --cache --cache-location=node_modules/.cache/eslint --cache-strategy content --concurrency=auto",
"lint-changed": "NODE_OPTIONS=--max_old_space_size=8192 ./scripts/lintChanged.sh",
"lint-watch": "npx eslint-watch --watch --changed",
"shellcheck": "./scripts/shellCheck.sh",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {format, parseISO} from 'date-fns';
import {Str} from 'expensify-common';
import React, {useMemo, useRef} from 'react';
import React, {useState} from 'react';
import {View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
Expand Down Expand Up @@ -32,7 +32,6 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import {getConnectedIntegration} from '@libs/PolicyUtils';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import Navigation from '@navigation/Navigation';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
Expand All @@ -53,51 +52,62 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
const {policyID, cardID, backTo} = route.params;
const feedName = decodeURIComponent(route.params.feed) as CompanyCardFeedWithDomainID;
const bank = getCompanyCardFeed(feedName);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const [shouldUseStagingServer = isUsingStagingApi()] = useOnyx(ONYXKEYS.SHOULD_USE_STAGING_SERVER);
const policy = usePolicy(policyID);
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const isUnassigningRef = useRef(false);

const {translate, getLocalDateFromDatetime} = useLocalize();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const illustrations = useThemeIllustrations();
const companyCardFeedIcons = useCompanyCardFeedIcons();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['FallbackAvatar', 'Hourglass', 'MoneySearch', 'RemoveMembers', 'Sync', 'Trashcan']);

const {isOffline} = useNetwork();
const accountingIntegrations = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES;
const syncingAccountingIntegration = accountingIntegrations.find((integration) => integration === connectionSyncProgress?.connectionName);
const connectedIntegration = getConnectedIntegration(policy, accountingIntegrations) ?? syncingAccountingIntegration;
const {showConfirmModal} = useConfirmModal();

const policy = usePolicy(policyID);
const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`);
const [customCardNames] = useOnyx(ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES);
const [shouldUseStagingServer = isUsingStagingApi()] = useOnyx(ONYXKEYS.SHOULD_USE_STAGING_SERVER);
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [allBankCards, allBankCardsMetadata] = useCardsList(feedName);
const [cardList, cardListMetadata] = useOnyx(ONYXKEYS.CARD_LIST);
const [cardFeeds] = useCardFeeds(policyID);

// Prefer feed-scoped card from WORKSPACE_CARDS_LIST to maintain proper access control
// Only use CARD_LIST as fallback if card is being unassigned (has pendingAction: DELETE)
const [isUnassigning, setIsUnassigning] = useState(false);

const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const syncingAccountingIntegration = CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES.find((integration) => integration === connectionSyncProgress?.connectionName);
const connectedIntegration = getConnectedIntegration(policy, CONST.POLICY.CONNECTIONS.ACCOUNTING_CONNECTION_NAMES) ?? syncingAccountingIntegration;

// Prefer feed-scoped card from WORKSPACE_CARDS_LIST to maintain proper access control.
// Only use CARD_LIST as fallback if card is being unassigned (has pendingAction: DELETE).
// This prevents showing cards from other feeds/workspaces via deep links while still
// preventing NotHerePage flash during the unassignment flow
// preventing NotHerePage flash during the unassignment flow.
const feedScopedCard = allBankCards?.[cardID];
const globalCard = cardList?.[cardID];
const isCardBeingUnassigned = globalCard?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const card = feedScopedCard ?? (isCardBeingUnassigned ? globalCard : undefined);

const cardBank = card?.bank;
const cardholder = personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID];
const displayName = getDisplayNameOrDefault(cardholder);
const exportMenuItem = getExportMenuItem(connectedIntegration, policyID, translate, policy, card);

const [cardFeeds] = useCardFeeds(policyID);
const companyFeeds = getCompanyFeeds(cardFeeds);
const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, companyFeeds[feedName]);
const plaidUrl = getPlaidInstitutionIconUrl(feedName);

// Show "Break connection" only when Mock Bank requests target non-production APIs.
const isMockBank = bank?.includes(CONST.COMPANY_CARDS.BANK_CONNECTIONS.MOCK_BANK);
const isUsingNonProductionAPI = shouldUseStagingServer || CONFIG.IS_USING_LOCAL_WEB;
const shouldShowBreakConnection = isMockBank && isUsingNonProductionAPI;

const lastScrape = card?.lastScrape
? format(getLocalDateFromDatetime(card.lastScrape), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING)
: translate('workspace.moreFeatures.companyCards.neverUpdated');

const errorRowStyles = [styles.ph5, styles.mb3];

const unassignCard = () => {
if (card) {
isUnassigningRef.current = true;
setIsUnassigning(true);
unassignWorkspaceCompanyCard(domainOrWorkspaceAccountID, bank, card);
}
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack());
Expand All @@ -111,25 +121,8 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
updateWorkspaceCompanyCard(domainOrWorkspaceAccountID, cardID, bank, card?.lastScrapeResult, true);
};

// Show "Break connection" only when Mock Bank requests target non-production APIs.
const isMockBank = bank?.includes(CONST.COMPANY_CARDS.BANK_CONNECTIONS.MOCK_BANK);
const isUsingNonProductionAPI = shouldUseStagingServer || CONFIG.IS_USING_LOCAL_WEB;
const shouldShowBreakConnection = isMockBank && isUsingNonProductionAPI;

const lastScrape = useMemo(() => {
if (!card?.lastScrape) {
return translate('workspace.moreFeatures.companyCards.neverUpdated');
}
return format(getLocalDateFromDatetime(card?.lastScrape), CONST.DATE.FNS_DATE_TIME_FORMAT_STRING);
}, [getLocalDateFromDatetime, card?.lastScrape, translate]);

const lastUpdatedActivityReasonAttributes: SkeletonSpanReasonAttributes = {
context: 'WorkspaceCompanyCardDetailsPage',
isLoadingLastUpdated: card?.isLoadingLastUpdated,
};

// Don't show NotFoundPage if card is being unassigned or data is still loading
if ((!card && !isUnassigningRef.current && !isLoadingOnyxValue(allBankCardsMetadata) && !isLoadingOnyxValue(cardListMetadata)) || (isCardBeingUnassigned && !isUnassigningRef.current)) {
// Don't show NotFoundPage if the card is being unassigned or data is still loading.
if ((!card && !isUnassigning && !isLoadingOnyxValue(allBankCardsMetadata) && !isLoadingOnyxValue(cardListMetadata)) || (isCardBeingUnassigned && !isUnassigning)) {
return <NotFoundPage />;
}

Expand All @@ -156,7 +149,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
) : (
<ImageSVG
contentFit="contain"
src={getCardFeedIcon(cardBank, illustrations, companyCardFeedIcons)}
src={getCardFeedIcon(card?.bank, illustrations, companyCardFeedIcons)}
pointerEvents="none"
height={variables.cardPreviewHeight}
width={variables.cardPreviewWidth}
Expand Down Expand Up @@ -193,7 +186,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
/>
<OfflineWithFeedback
pendingAction={card?.nameValuePairs?.pendingFields?.cardTitle}
errorRowStyles={[styles.ph5, styles.mb3]}
errorRowStyles={errorRowStyles}
errors={getLatestErrorField(card?.nameValuePairs ?? {}, 'cardTitle')}
onClose={() => clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'cardTitle')}
>
Expand All @@ -209,7 +202,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
{exportMenuItem?.shouldShowMenuItem ? (
<OfflineWithFeedback
pendingAction={exportMenuItem?.exportType ? card?.nameValuePairs?.pendingFields?.[exportMenuItem.exportType] : undefined}
errorRowStyles={[styles.ph5, styles.mb3]}
errorRowStyles={errorRowStyles}
errors={exportMenuItem.exportType ? getLatestErrorField(card?.nameValuePairs ?? {}, exportMenuItem.exportType) : undefined}
onClose={() => {
if (!exportMenuItem.exportType) {
Expand All @@ -232,7 +225,10 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
rightComponent={
<ActivityIndicator
style={[styles.popoverMenuIcon]}
reasonAttributes={lastUpdatedActivityReasonAttributes}
reasonAttributes={{
context: 'WorkspaceCompanyCardDetailsPage',
isLoadingLastUpdated: card?.isLoadingLastUpdated,
}}
/>
}
description={translate('workspace.moreFeatures.companyCards.lastUpdated')}
Expand All @@ -241,7 +237,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
/>
<OfflineWithFeedback
pendingAction={card?.pendingFields?.scrapeMinDate}
errorRowStyles={[styles.ph5, styles.mb3]}
errorRowStyles={errorRowStyles}
errors={getLatestErrorField(card ?? {}, 'scrapeMinDate')}
onClose={() => clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'scrapeMinDate', true)}
>
Expand Down Expand Up @@ -269,7 +265,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag
/>
<OfflineWithFeedback
pendingAction={card?.pendingFields?.lastScrape}
errorRowStyles={[styles.ph5, styles.mb3]}
errorRowStyles={errorRowStyles}
errors={getLatestErrorField(card ?? {}, 'lastScrape')}
onClose={() => clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'lastScrape', true)}
>
Expand Down
Loading