diff --git a/package.json b/package.json index 0f4b18023910..f01461d4f4b6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx index 79d9fba7542a..3c0ac6dc7e09 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage.tsx @@ -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'; @@ -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'; @@ -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()); @@ -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 ; } @@ -156,7 +149,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag ) : ( clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'cardTitle')} > @@ -209,7 +202,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag {exportMenuItem?.shouldShowMenuItem ? ( { if (!exportMenuItem.exportType) { @@ -232,7 +225,10 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag rightComponent={ } description={translate('workspace.moreFeatures.companyCards.lastUpdated')} @@ -241,7 +237,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag /> clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'scrapeMinDate', true)} > @@ -269,7 +265,7 @@ function WorkspaceCompanyCardDetailsPage({route}: WorkspaceCompanyCardDetailsPag /> clearCompanyCardErrorField(domainOrWorkspaceAccountID, cardID, bank, 'lastScrape', true)} >