From 389274dbbf514662f266930211e9be5b31a57ca7 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 5 Mar 2026 17:18:47 +0100 Subject: [PATCH 01/23] Add "Assign new cards" option in feed settings for direct feeds Direct card feeds cache their accountList at initial connection, so newly issued cards don't appear in the assignment UI. This adds a menu item in the feed settings page (only for direct/OAuth/Plaid feeds) that triggers the existing bank reconnection flow to refresh the card list. --- src/languages/en.ts | 2 + src/languages/es.ts | 2 + .../WorkspaceCompanyCardsSettingsPage.tsx | 50 ++++++++++++++++++- 3 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b9813b075f676..c0f9844c29bb0 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5194,6 +5194,8 @@ const translations = { removeCardFeed: 'Remove card feed', removeCardFeedTitle: (feedName: string) => `Remove ${feedName} feed`, removeCardFeedDescription: 'Are you sure you want to remove this card feed? This will unassign all cards.', + assignNewCards: 'Assign new cards', + assignNewCardsDescription: 'Get the latest cards to assign from your bank', error: { feedNameRequired: 'Card feed name is required', statementCloseDateRequired: 'Please select a statement close date.', diff --git a/src/languages/es.ts b/src/languages/es.ts index d36023e9b4f8d..1af16ac361f87 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5043,6 +5043,8 @@ ${amount} para ${merchant} - ${date}`, removeCardFeed: 'Quitar la alimentación de tarjetas', removeCardFeedTitle: (feedName) => `Eliminar el feed de ${feedName}`, removeCardFeedDescription: '¿Estás seguro de que deseas eliminar esta fuente de tarjetas? Esto anulará la asignación de todas las tarjetas.', + assignNewCards: 'Asignar nuevas tarjetas', + assignNewCardsDescription: 'Obtén las últimas tarjetas de tu banco para asignar', error: { feedNameRequired: 'Se requiere el nombre de la fuente de la tarjeta', statementCloseDateRequired: 'Por favor, selecciona una fecha de cierre del estado de cuenta.', diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index e954f57c03be8..19fdf966d8957 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -11,17 +11,28 @@ import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import useCardFeeds from '@hooks/useCardFeeds'; import useCardsList from '@hooks/useCardsList'; +import {useCurrencyListState} from '@hooks/useCurrencyList'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import {deleteWorkspaceCompanyCardFeed, setWorkspaceCompanyCardTransactionLiability} from '@libs/actions/CompanyCards'; -import {getCompanyCardFeed, getCompanyFeeds, getCustomOrFormattedFeedName, getDomainOrWorkspaceAccountID, getSelectedFeed} from '@libs/CardUtils'; +import {deleteWorkspaceCompanyCardFeed, setAssignCardStepAndData, setWorkspaceCompanyCardTransactionLiability} from '@libs/actions/CompanyCards'; +import { + getCompanyCardFeed, + getCompanyFeeds, + getCustomOrFormattedFeedName, + getDomainOrWorkspaceAccountID, + getPlaidCountry, + getPlaidInstitutionId, + getSelectedFeed, + isDirectFeed, +} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -55,6 +66,9 @@ function WorkspaceCompanyCardsSettingsPage({ const isPersonal = liabilityType === CONST.COMPANY_CARDS.DELETE_TRANSACTIONS.ALLOW; const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeedData); const isPending = !!selectedFeedData?.pending; + const {currencyList} = useCurrencyListState(); + const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); + const isDirectCardFeed = isDirectFeed(feed); const statementCloseDate = useMemo(() => { if (!selectedFeedData?.statementPeriodEndDay) { return undefined; @@ -91,6 +105,30 @@ function WorkspaceCompanyCardsSettingsPage({ } }; + const openBankConnectionFlow = () => { + if (!selectedFeed) { + return; + } + + const institutionId = getPlaidInstitutionId(selectedFeed); + const initialStep = institutionId ? CONST.COMPANY_CARD.STEP.PLAID_CONNECTION : CONST.COMPANY_CARD.STEP.BANK_CONNECTION; + + if (institutionId) { + const country = getPlaidCountry(policy?.outputCurrency, currencyList, countryByIp); + setAddNewCompanyCardStepAndData({ + data: { + selectedCountry: country, + }, + }); + } + + setAssignCardStepAndData({currentStep: initialStep}); + + Navigation.setNavigationActionToMicrotaskQueue(() => { + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION.getRoute(policyID, selectedFeed)); + }); + }; + const onToggleLiability = (isOn: boolean) => { if (!feed) { return; @@ -148,6 +186,14 @@ function WorkspaceCompanyCardsSettingsPage({ /> {translate('workspace.moreFeatures.companyCards.setTransactionLiabilityDescription')} + {isDirectCardFeed && ( + + )} Date: Thu, 5 Mar 2026 20:00:20 +0100 Subject: [PATCH 02/23] Add refresh card feed connection route and page for direct feeds Create a dedicated RefreshCardFeedConnectionPage that reuses BankConnection/PlaidConnectionStep with an isRefreshFlow prop. When isRefreshFlow is true, BankConnection always opens the bank auth and dismisses the modal after re-auth instead of proceeding to the assign card flow. --- src/ROUTES.ts | 4 ++ src/SCREENS.ts | 1 + .../ModalStackNavigators/index.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 4 ++ .../companyCards/BankConnection/index.tsx | 19 ++++++- .../RefreshCardFeedConnectionPage.tsx | 55 +++++++++++++++++++ .../WorkspaceCompanyCardsSettingsPage.tsx | 2 +- src/pages/workspace/withPolicy.tsx | 1 + 9 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index afa59492cd285..a79b4c76de203 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2399,6 +2399,10 @@ const ROUTES = { route: 'workspaces/:policyID/company-cards/:feed/broken-card-feed-connection', getRoute: (policyID: string, feed: CompanyCardFeedWithDomainID) => `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/broken-card-feed-connection` as const, }, + WORKSPACE_COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION: { + route: 'workspaces/:policyID/company-cards/:feed/refresh-card-feed-connection', + getRoute: (policyID: string, feed: CompanyCardFeedWithDomainID) => `workspaces/${policyID}/company-cards/${encodeURIComponent(feed)}/refresh-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) => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2cb34a04bd612..eec8e8eab2630 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -628,6 +628,7 @@ const SCREENS = { PROFILE: 'Workspace_Overview', COMPANY_CARDS: 'Workspace_CompanyCards', COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION: 'Workspace_CompanyCards_BrokenCardFeedConnection', + COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION: 'Workspace_CompanyCards_RefreshCardFeedConnection', 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', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 4a245505e47d4..a08e64289c978 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -772,6 +772,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, [SCREENS.WORKSPACE.INVOICES_VERIFY_ACCOUNT]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicesVerifyAccountPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION]: () => require('../../../../pages/workspace/companyCards/BrokenCardFeedConnectionPage').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION]: () => require('../../../../pages/workspace/companyCards/RefreshCardFeedConnectionPage').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]: () => diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 236214e627ae7..a732b1a10c53c 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -859,6 +859,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION.route, }, + [SCREENS.WORKSPACE.COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_REFRESH_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 07e72fa0ab414..fac17de350724 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1287,6 +1287,10 @@ type SettingsNavigatorParamList = { // 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_CARDS_REFRESH_CARD_FEED_CONNECTION]: { + policyID: string; + feed: CompanyCardFeedWithDomainID; + }; [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_ASSIGNEE]: { policyID: string; feed: CompanyCardFeedWithDomainID; diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index e6be08c479e13..33378730a03e2 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -43,9 +43,12 @@ type BankConnectionProps = { /** Route params for add new card flow */ route?: PlatformStackRouteProp; + + /** Whether this is a refresh card list flow — always opens bank connection and dismisses on re-auth */ + isRefreshFlow?: boolean; }; -function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnectionProps) { +function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshFlow}: BankConnectionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); @@ -120,6 +123,19 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti return; } + // Handle refresh card list flow — always open bank connection, dismiss on re-auth + if (feed && isRefreshFlow) { + if (!isFeedExpired) { + customWindow?.close(); + Navigation.closeRHPFlow(); + return; + } + if (!isPlaid && url) { + customWindow = openBankConnection(url); + } + return; + } + // Handle assign card flow if (feed) { if (!isFeedExpired) { @@ -190,6 +206,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + isRefreshFlow, ]); const getContent = () => { diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx new file mode 100644 index 0000000000000..95dd137d848e4 --- /dev/null +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -0,0 +1,55 @@ +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 PlaidConnectionStep from './addNew/PlaidConnectionStep'; +import BankConnection from './BankConnection'; + +type RefreshCardFeedConnectionPageProps = PlatformStackScreenProps & + WithPolicyAndFullscreenLoadingProps; + +function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectionPageProps) { + const feed = route.params?.feed; + 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(RefreshCardFeedConnectionPage); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index 19fdf966d8957..706803957ffb3 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -125,7 +125,7 @@ function WorkspaceCompanyCardsSettingsPage({ setAssignCardStepAndData({currentStep: initialStep}); Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION.getRoute(policyID, selectedFeed)); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION.getRoute(policyID, selectedFeed)); }); }; diff --git a/src/pages/workspace/withPolicy.tsx b/src/pages/workspace/withPolicy.tsx index b847b6b40f001..def91080f508e 100644 --- a/src/pages/workspace/withPolicy.tsx +++ b/src/pages/workspace/withPolicy.tsx @@ -53,6 +53,7 @@ type PolicyRouteName = | typeof SCREENS.WORKSPACE.RULES | typeof SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW | typeof SCREENS.WORKSPACE.COMPANY_CARDS_BROKEN_CARD_FEED_CONNECTION + | typeof SCREENS.WORKSPACE.COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION | typeof SCREENS.WORKSPACE.ACCOUNTING.CLAIM_OFFER | typeof SCREENS.WORKSPACE.TIME_TRACKING; From adfff167dab5582836e4f49a9386e12ef4b5ae91 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 5 Mar 2026 21:16:01 +0100 Subject: [PATCH 03/23] Add refresh connection success messages and update related components Enhance user experience by adding success messages for refreshed bank connections in both English and Spanish translations. Update the RefreshCardFeedConnectionPage and related components to handle the new success state, ensuring proper navigation and user feedback upon successful re-authentication. --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ .../BankConnection/index.native.tsx | 14 +++++++- .../companyCards/BankConnection/index.tsx | 36 ++++++++----------- .../RefreshCardFeedConnectionPage.tsx | 33 +++++++++++++++-- .../addNew/PlaidConnectionStep.tsx | 4 +-- 6 files changed, 64 insertions(+), 27 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index c0f9844c29bb0..41ca0390fbccd 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5196,6 +5196,8 @@ const translations = { removeCardFeedDescription: 'Are you sure you want to remove this card feed? This will unassign all cards.', assignNewCards: 'Assign new cards', assignNewCardsDescription: 'Get the latest cards to assign from your bank', + refreshConnectionSuccess: 'Connection refreshed', + refreshConnectionSuccessDescription: 'Your bank connection has been re-authenticated successfully. You can now assign new cards.', error: { feedNameRequired: 'Card feed name is required', statementCloseDateRequired: 'Please select a statement close date.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1af16ac361f87..3c04e688240d3 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5045,6 +5045,8 @@ ${amount} para ${merchant} - ${date}`, removeCardFeedDescription: '¿Estás seguro de que deseas eliminar esta fuente de tarjetas? Esto anulará la asignación de todas las tarjetas.', assignNewCards: 'Asignar nuevas tarjetas', assignNewCardsDescription: 'Obtén las últimas tarjetas de tu banco para asignar', + refreshConnectionSuccess: 'Conexión actualizada', + refreshConnectionSuccessDescription: 'La conexión con tu banco ha sido reautenticada exitosamente. Ahora puedes asignar nuevas tarjetas.', error: { feedNameRequired: 'Se requiere el nombre de la fuente de la tarjeta', statementCloseDateRequired: 'Por favor, selecciona una fecha de cierre del estado de cuenta.', diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 9bc6a5008ccf1..cc84780867f33 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -39,9 +39,15 @@ type BankConnectionProps = { /** Route params for add new card flow */ route?: PlatformStackRouteProp; + + /** Whether this is a refresh card list flow */ + isRefreshConnectionFlow?: boolean; + + /** Called when re-authentication completes successfully in a refresh flow */ + onRefreshComplete?: () => void; }; -function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnectionProps) { +function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onRefreshComplete}: BankConnectionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const webViewRef = useRef(null); @@ -109,6 +115,10 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); return; } + if (isRefreshConnectionFlow && onRefreshComplete) { + onRefreshComplete(); + return; + } setAssignCardStepAndData({ currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, isEditing: false, @@ -148,6 +158,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, + isRefreshConnectionFlow, + onRefreshComplete, ]); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 33378730a03e2..d415150da0e33 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -45,10 +45,13 @@ type BankConnectionProps = { route?: PlatformStackRouteProp; /** Whether this is a refresh card list flow — always opens bank connection and dismisses on re-auth */ - isRefreshFlow?: boolean; + isRefreshConnectionFlow?: boolean; + + /** Called when re-authentication completes successfully in a refresh flow */ + onRefreshComplete?: () => void; }; -function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshFlow}: BankConnectionProps) { +function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onRefreshComplete}: BankConnectionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); @@ -73,7 +76,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshFlow const url = getCompanyCardBankConnection(policyID, bankName); const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.[feed]) : false; const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; - const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; + const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); @@ -123,19 +126,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshFlow return; } - // Handle refresh card list flow — always open bank connection, dismiss on re-auth - if (feed && isRefreshFlow) { - if (!isFeedExpired) { - customWindow?.close(); - Navigation.closeRHPFlow(); - return; - } - if (!isPlaid && url) { - customWindow = openBankConnection(url); - } - return; - } - // Handle assign card flow if (feed) { if (!isFeedExpired) { @@ -145,19 +135,20 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshFlow Navigation.closeRHPFlow(); return; } + if (isRefreshConnectionFlow && onRefreshComplete) { + onRefreshComplete(); + return; + } setAssignCardStepAndData({ currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, isEditing: false, }); return; } - if (isPlaid) { - return; - } - if (url) { + if (!isPlaid && url) { customWindow = openBankConnection(url); - return; } + return; } // Handle add new card flow @@ -206,7 +197,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshFlow isFeedConnectionBroken, updateBrokenConnection, isNewFeedHasError, - isRefreshFlow, + isRefreshConnectionFlow, + onRefreshComplete, ]); const getContent = () => { diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx index 95dd137d848e4..2dfc265b2c3d2 100644 --- a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -1,6 +1,10 @@ -import React, {useEffect} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; +import ConfirmationPage from '@components/ConfirmationPage'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import LoadingPage from '@pages/LoadingPage'; @@ -24,6 +28,7 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); const currentStep = assignCard?.currentStep; + const [isRefreshComplete, setIsRefreshComplete] = useState(false); useEffect(() => { return () => { @@ -31,13 +36,36 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio }; }, []); + const handleRefreshComplete = useCallback(() => { + setIsRefreshComplete(true); + }, []); + + if (isRefreshComplete) { + return ( + + Navigation.dismissModal()} + /> + Navigation.dismissModal()} + /> + + ); + } + switch (currentStep) { case CONST.COMPANY_CARD.STEP.BANK_CONNECTION: return ( ); case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: @@ -45,6 +73,7 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio ); default: diff --git a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx index 1da4cdcb44a84..080ed79366ccf 100644 --- a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx +++ b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx @@ -26,7 +26,7 @@ import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {CardFeedWithNumber} from '@src/types/onyx/CardFeeds'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -function PlaidConnectionStep({feed, policyID, onExit}: {feed?: CompanyCardFeedWithDomainID; policyID?: string; onExit?: () => void}) { +function PlaidConnectionStep({feed, policyID, onExit, isRefreshConnectionFlow}: {feed?: CompanyCardFeedWithDomainID; policyID?: string; onExit?: () => void; isRefreshConnectionFlow?: boolean}) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true}); @@ -210,7 +210,7 @@ function PlaidConnectionStep({feed, policyID, onExit}: {feed?: CompanyCardFeedWi shouldEnableMaxHeight > {isPlaidDisabled ? ( From 85e432c505c543e0bb53c183d27ad608a40d7341 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 5 Mar 2026 21:21:40 +0100 Subject: [PATCH 04/23] Refactor navigation logic for broken feed connections in BankConnection component Update the BankConnection component to ensure proper navigation when a feed connection is broken. The logic for handling broken connections has been streamlined to avoid redundancy, improving code clarity and maintainability. Additionally, adjust header titles to reflect the correct context based on the refresh connection flow. --- .../companyCards/BankConnection/index.native.tsx | 6 ++++-- .../workspace/companyCards/BankConnection/index.tsx | 10 ++++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index cc84780867f33..ab6fdae11e29e 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -112,13 +112,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn if (feed && !isFeedExpired) { if (isFeedConnectionBroken) { updateBrokenConnection(); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - return; } if (isRefreshConnectionFlow && onRefreshComplete) { onRefreshComplete(); return; } + if (isFeedConnectionBroken) { + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + return; + } setAssignCardStepAndData({ currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, isEditing: false, diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index d415150da0e33..01a89925cf44d 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -75,8 +75,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const url = getCompanyCardBankConnection(policyID, bankName); const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.[feed]) : false; - const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; - const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; + const headerTitleAddCards = !backTo ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.addCards') : undefined; + const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); @@ -132,13 +132,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn customWindow?.close(); if (isFeedConnectionBroken) { updateBrokenConnection(); - Navigation.closeRHPFlow(); - return; } if (isRefreshConnectionFlow && onRefreshComplete) { onRefreshComplete(); return; } + if (isFeedConnectionBroken) { + Navigation.closeRHPFlow(); + return; + } setAssignCardStepAndData({ currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, isEditing: false, From 1a0766152ea1222da1ca7ff7d953297f02387102 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 5 Mar 2026 21:51:02 +0100 Subject: [PATCH 05/23] Add new card assignment and connection refresh messages in multiple languages --- src/languages/de.ts | 4 ++++ src/languages/fr.ts | 4 ++++ src/languages/it.ts | 4 ++++ src/languages/ja.ts | 4 ++++ src/languages/nl.ts | 4 ++++ src/languages/pl.ts | 4 ++++ src/languages/pt-BR.ts | 4 ++++ src/languages/zh-hans.ts | 4 ++++ 8 files changed, 32 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 8a6be1581b8f4..cabcd15f02e95 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5365,6 +5365,10 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU removeCardFeed: 'Kartenfeed entfernen', removeCardFeedTitle: (feedName: string) => `${feedName}-Feed entfernen`, removeCardFeedDescription: 'Möchtest du diesen Kartenfeed wirklich entfernen? Dadurch werden alle Karten zugewiesen.', + assignNewCards: 'Neue Karten zuweisen', + assignNewCardsDescription: 'Die neuesten Karten von deiner Bank zum Zuweisen abrufen', + refreshConnectionSuccess: 'Verbindung aktualisiert', + refreshConnectionSuccessDescription: 'Deine Bankverbindung wurde erfolgreich erneut authentifiziert. Du kannst jetzt neue Karten zuweisen.', error: { feedNameRequired: 'Name des Kartenfeeds ist erforderlich', statementCloseDateRequired: 'Bitte wählen Sie ein Abrechnungsenddatum aus.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 0b82ad790797b..0826a7ffd9265 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5384,6 +5384,10 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. removeCardFeed: 'Supprimer le flux de cartes', removeCardFeedTitle: (feedName: string) => `Supprimer le flux ${feedName}`, removeCardFeedDescription: "Voulez-vous vraiment supprimer ce flux de cartes ? Cela retirera l'assignation de toutes les cartes.", + assignNewCards: 'Attribuer de nouvelles cartes', + assignNewCardsDescription: 'Obtenez les dernières cartes à attribuer depuis votre banque', + refreshConnectionSuccess: 'Connexion actualisée', + refreshConnectionSuccessDescription: 'Votre connexion bancaire a été ré-authentifiée avec succès. Vous pouvez maintenant attribuer de nouvelles cartes.', error: { feedNameRequired: 'Le nom du flux de carte est obligatoire', statementCloseDateRequired: 'Veuillez sélectionner une date de clôture de relevé.', diff --git a/src/languages/it.ts b/src/languages/it.ts index d273792b023f0..2ae7f6eef24c4 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5354,6 +5354,10 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. removeCardFeed: 'Rimuovi flusso carta', removeCardFeedTitle: (feedName: string) => `Rimuovi feed ${feedName}`, removeCardFeedDescription: 'Sei sicuro di voler rimuovere questo flusso di carte? Questo rimuoverà l’assegnazione di tutte le carte.', + assignNewCards: 'Assegna nuove carte', + assignNewCardsDescription: 'Ottieni le ultime carte da assegnare dalla tua banca', + refreshConnectionSuccess: 'Connessione aggiornata', + refreshConnectionSuccessDescription: 'La connessione bancaria è stata riautenticata con successo. Ora puoi assegnare nuove carte.', error: { feedNameRequired: 'Il nome del feed della carta è obbligatorio', statementCloseDateRequired: 'Seleziona una data di chiusura dell’estratto conto.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 0a811ba9bb8c5..e338e67a6b620 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5297,6 +5297,10 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO removeCardFeed: 'カードフィードを削除', removeCardFeedTitle: (feedName: string) => `${feedName}フィードを削除`, removeCardFeedDescription: 'このカードフィードを削除してもよろしいですか?すべてのカードの割り当てが解除されます。', + assignNewCards: '新しいカードを割り当てる', + assignNewCardsDescription: '銀行から最新のカードを取得して割り当てます', + refreshConnectionSuccess: '接続が更新されました', + refreshConnectionSuccessDescription: '銀行接続の再認証が正常に完了しました。新しいカードを割り当てることができます。', error: { feedNameRequired: 'カードフィード名は必須です', statementCloseDateRequired: '明細書の締め日を選択してください。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6ffe1f3c1a213..8a2afccdf440e 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5336,6 +5336,10 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ removeCardFeed: 'Kaartfeed verwijderen', removeCardFeedTitle: (feedName: string) => `Feed ${feedName} verwijderen`, removeCardFeedDescription: 'Weet je zeker dat je deze kaartfeed wilt verwijderen? Hierdoor worden alle kaarten losgekoppeld.', + assignNewCards: 'Nieuwe kaarten toewijzen', + assignNewCardsDescription: 'Haal de nieuwste kaarten op van je bank om toe te wijzen', + refreshConnectionSuccess: 'Verbinding vernieuwd', + refreshConnectionSuccessDescription: 'Je bankverbinding is succesvol opnieuw geverifieerd. Je kunt nu nieuwe kaarten toewijzen.', error: { feedNameRequired: 'Naam van kaartfeed is vereist', statementCloseDateRequired: 'Selecteer een afsluitdatum voor het afschrift.', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5fac6256ad35a..54011e1d21bce 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5331,6 +5331,10 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy removeCardFeed: 'Usuń źródło karty', removeCardFeedTitle: (feedName: string) => `Usuń strumień ${feedName}`, removeCardFeedDescription: 'Na pewno chcesz usunąć ten kanał kart? Spowoduje to odłączenie wszystkich kart.', + assignNewCards: 'Przypisz nowe karty', + assignNewCardsDescription: 'Pobierz najnowsze karty z banku do przypisania', + refreshConnectionSuccess: 'Połączenie odświeżone', + refreshConnectionSuccessDescription: 'Połączenie z bankiem zostało pomyślnie ponownie uwierzytelnione. Możesz teraz przypisać nowe karty.', error: { feedNameRequired: 'Nazwa źródła karty jest wymagana', statementCloseDateRequired: 'Wybierz datę zamknięcia wyciągu.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index a441fe5c534ce..f1eb23a65959d 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5336,6 +5336,10 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS removeCardFeed: 'Remover feed do cartão', removeCardFeedTitle: (feedName: string) => `Remover feed ${feedName}`, removeCardFeedDescription: 'Tem certeza de que deseja remover este feed de cartão? Isso removerá a atribuição de todos os cartões.', + assignNewCards: 'Atribuir novos cartões', + assignNewCardsDescription: 'Obtenha os cartões mais recentes do seu banco para atribuir', + refreshConnectionSuccess: 'Conexão atualizada', + refreshConnectionSuccessDescription: 'Sua conexão bancária foi reautenticada com sucesso. Agora você pode atribuir novos cartões.', error: { feedNameRequired: 'O nome do feed do cartão é obrigatório', statementCloseDateRequired: 'Selecione uma data de fechamento do extrato.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 3f3090d975a42..9fae05fea14ee 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5216,6 +5216,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM removeCardFeed: '移除卡片流水', removeCardFeedTitle: (feedName: string) => `移除 ${feedName} 数据源`, removeCardFeedDescription: '确定要移除此卡片数据源吗?这将取消分配所有卡片。', + assignNewCards: '分配新卡片', + assignNewCardsDescription: '从银行获取最新的卡片进行分配', + refreshConnectionSuccess: '连接已刷新', + refreshConnectionSuccessDescription: '您的银行连接已成功重新验证。您现在可以分配新卡片。', error: { feedNameRequired: '必须填写卡片流水名称', statementCloseDateRequired: '请选择账单结算日期。', From 8dd5494028c7c1f4db3dbb099c64beb26b359b26 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 5 Mar 2026 22:02:57 +0100 Subject: [PATCH 06/23] Refactor PlaidConnectionStep component for improved readability --- .../companyCards/addNew/PlaidConnectionStep.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx index 8bae5285c28d2..9ad45ada8e834 100644 --- a/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx +++ b/src/pages/workspace/companyCards/addNew/PlaidConnectionStep.tsx @@ -26,7 +26,17 @@ import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import type {CardFeedWithNumber} from '@src/types/onyx/CardFeeds'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -function PlaidConnectionStep({feed, policyID, onExit, isRefreshConnectionFlow}: {feed?: CompanyCardFeedWithDomainID; policyID?: string; onExit?: () => void; isRefreshConnectionFlow?: boolean}) { +function PlaidConnectionStep({ + feed, + policyID, + onExit, + isRefreshConnectionFlow, +}: { + feed?: CompanyCardFeedWithDomainID; + policyID?: string; + onExit?: () => void; + isRefreshConnectionFlow?: boolean; +}) { const {translate} = useLocalize(); const styles = useThemeStyles(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); From 30fbb7a43c09ccf4f08a216372cc1e2b05cbbb2c Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 5 Mar 2026 22:12:55 +0100 Subject: [PATCH 07/23] Update WorkspaceCompanyCardsSettingsPage to include Sync icon in card assignment menu --- .../companyCards/WorkspaceCompanyCardsSettingsPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index a10ca41c64b7a..48632d29a7cfb 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -59,7 +59,7 @@ function WorkspaceCompanyCardsSettingsPage({ const feed = selectedFeed ? getCompanyCardFeed(selectedFeed) : undefined; const [cardsList] = useCardsList(selectedFeed); - const icons = useMemoizedLazyExpensifyIcons(['Trashcan'] as const); + const icons = useMemoizedLazyExpensifyIcons(['Sync', 'Trashcan'] as const); const feedName = selectedFeed ? getCustomOrFormattedFeedName(translate, feed, cardFeeds?.[selectedFeed]?.customFeedName) : undefined; const companyFeeds = getCompanyFeeds(cardFeeds); const selectedFeedData = selectedFeed ? companyFeeds[selectedFeed] : undefined; @@ -189,7 +189,7 @@ function WorkspaceCompanyCardsSettingsPage({ {isDirectCardFeed && ( Date: Fri, 6 Mar 2026 00:26:09 +0100 Subject: [PATCH 08/23] Add RefreshCardFeedConnection test suite for comprehensive coverage Introduce a new test file for the RefreshCardFeedConnection component, implementing various test cases to ensure correct rendering and functionality based on different connection steps. The tests cover loading states, bank connection rendering, and plaid connection handling, enhancing overall test coverage and reliability. --- tests/ui/RefreshCardFeedConnection.tsx | 326 +++++++++++++++++++++++++ 1 file changed, 326 insertions(+) create mode 100644 tests/ui/RefreshCardFeedConnection.tsx diff --git a/tests/ui/RefreshCardFeedConnection.tsx b/tests/ui/RefreshCardFeedConnection.tsx new file mode 100644 index 0000000000000..0fcde0a1dd983 --- /dev/null +++ b/tests/ui/RefreshCardFeedConnection.tsx @@ -0,0 +1,326 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {PortalProvider} from '@gorhom/portal'; +import {NavigationContainer} from '@react-navigation/native'; +import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import ComposeProviders from '@components/ComposeProviders'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import OnyxListItemProvider from '@components/OnyxListItemProvider'; +import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; +import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; +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 RefreshCardFeedConnectionPage from '@pages/workspace/companyCards/RefreshCardFeedConnectionPage'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +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 WORKSPACE_ACCOUNT_ID = 5678; +const POLICY_ID = LHNTestUtils.getFakePolicy().id; +const DIRECT_FEED = `oauth.chase.com#${WORKSPACE_ACCOUNT_ID}` as CompanyCardFeedWithDomainID; + +TestHelper.setupGlobalFetchMock(); + +jest.mock('@hooks/useNetwork', () => + jest.fn(() => ({ + isOffline: false, + })), +); + +jest.mock('react-native-permissions', () => ({ + RESULTS: {UNAVAILABLE: 'unavailable', BLOCKED: 'blocked', DENIED: 'denied', GRANTED: 'granted', LIMITED: 'limited'}, + check: jest.fn(() => Promise.resolve('granted')), + request: jest.fn(() => Promise.resolve('granted')), + PERMISSIONS: {IOS: {CONTACTS: 'ios.permission.CONTACTS'}, ANDROID: {READ_CONTACTS: 'android.permission.READ_CONTACTS'}}, +})); + +jest.mock('@rnmapbox/maps', () => ({default: jest.fn(), MarkerView: jest.fn(), setAccessToken: jest.fn()})); + +jest.mock('react-native-plaid-link-sdk', () => ({dismissLink: jest.fn(), openLink: jest.fn(), usePlaidEmitter: jest.fn()})); + +jest.mock('@libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + goBack: jest.fn(), + closeRHPFlow: jest.fn(), + dismissModal: jest.fn(), + setNavigationActionToMicrotaskQueue: jest.fn((callback: () => void) => callback?.()), + getTopmostReportId: jest.fn(), +})); + +const mockClearAssignCardStepAndData = jest.fn(); + +jest.mock('@userActions/CompanyCards', () => ({ + setAssignCardStepAndData: jest.fn(), + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + clearAssignCardStepAndData: (...args: unknown[]) => mockClearAssignCardStepAndData(...args), + setAddNewCompanyCardStepAndData: jest.fn(), +})); + +let capturedOnRefreshComplete: (() => void) | undefined; +jest.mock('@pages/workspace/companyCards/BankConnection', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const {View, Text} = require('react-native'); + return { + __esModule: true, + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + default: ({onRefreshComplete, isRefreshConnectionFlow}: {onRefreshComplete?: () => void; isRefreshConnectionFlow?: boolean}) => { + capturedOnRefreshComplete = onRefreshComplete; + return ( + + {isRefreshConnectionFlow ? 'refresh-mode' : 'normal-mode'} + + ); + }, + }; +}); + +jest.mock('@pages/workspace/companyCards/addNew/PlaidConnectionStep', () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const {View} = require('react-native'); + return { + __esModule: true, + default: () => , + }; +}); + +const Stack = createPlatformStackNavigator(); + +const renderRefreshPage = () => { + return render( + + + + + + + + + , + ); +}; + +describe('RefreshCardFeedConnection', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(() => { + capturedOnRefreshComplete = undefined; + jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ + isSmallScreenWidth: false, + shouldUseNarrowLayout: false, + } as ResponsiveLayoutResult); + }); + + afterEach(async () => { + await act(async () => { + await Onyx.clear(); + }); + jest.clearAllMocks(); + }); + + describe('Step rendering', () => { + it('should show loading page when no step is set', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + await waitFor(() => { + expect(screen.getByText('Assign new cards')).toBeOnTheScreen(); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should render BankConnection when step is BANK_CONNECTION', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + await waitFor(() => { + expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); + expect(screen.getByText('refresh-mode')).toBeOnTheScreen(); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should render PlaidConnectionStep when step is PLAID_CONNECTION', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.PLAID_CONNECTION}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + await waitFor(() => { + expect(screen.getByTestId('PlaidConnectionStep')).toBeOnTheScreen(); + }); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + }); + + describe('Success view', () => { + it('should show success confirmation when onRefreshComplete is called', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + await waitFor(() => { + expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); + }); + + expect(capturedOnRefreshComplete).toBeDefined(); + + // Simulate BankConnection calling onRefreshComplete after successful re-auth + act(() => { + capturedOnRefreshComplete?.(); + }); + + await waitFor(() => { + expect(screen.getByText('Connection refreshed')).toBeOnTheScreen(); + expect(screen.getByTestId('confirmation-primary-button')).toBeOnTheScreen(); + }); + + // BankConnection should no longer be rendered + expect(screen.queryByTestId('BankConnection')).toBeNull(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should dismiss modal when Got it button is pressed', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + // Trigger the success view + act(() => { + capturedOnRefreshComplete?.(); + }); + + await waitFor(() => { + expect(screen.getByTestId('confirmation-primary-button')).toBeOnTheScreen(); + }); + + // Press the "Got it" button + fireEvent.press(screen.getByTestId('confirmation-primary-button')); + + expect(Navigation.dismissModal).toHaveBeenCalled(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + + it('should dismiss modal when back button is pressed on success view', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + act(() => { + capturedOnRefreshComplete?.(); + }); + + await waitFor(() => { + expect(screen.getByText('Connection refreshed')).toBeOnTheScreen(); + }); + + const backButton = screen.getByLabelText('Back'); + fireEvent.press(backButton); + + expect(Navigation.dismissModal).toHaveBeenCalled(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + }); + }); + + describe('Cleanup', () => { + it('should call clearAssignCardStepAndData on unmount', async () => { + await TestHelper.signInWithTestUser(); + + const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; + + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); + await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + }); + + const {unmount} = renderRefreshPage(); + await waitForBatchedUpdatesWithAct(); + + unmount(); + await waitForBatchedUpdatesWithAct(); + + expect(mockClearAssignCardStepAndData).toHaveBeenCalled(); + }); + }); +}); From 1329a71f3ab360693bb655dd88929db784f2b937 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Fri, 6 Mar 2026 01:01:56 +0100 Subject: [PATCH 09/23] Refactor BankConnection component to update broken connection handling Updated the logic for handling broken feed connections in the BankConnection component. The update ensures that the `updateBrokenConnection` function is called appropriately during both the assign card flow and the refresh connection flow, improving the reliability of the connection handling. Additionally, modified the header title logic to reflect the current flow more accurately. --- .../companyCards/BankConnection/index.native.tsx | 9 +++++---- .../workspace/companyCards/BankConnection/index.tsx | 7 ++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 2c3532fa089a0..a1df71c585775 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -70,7 +70,7 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); - const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; + const headerTitleAddCards = !backTo ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); @@ -110,14 +110,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn // Handle assign card flow if (feed && !isFeedExpired) { - if (isFeedConnectionBroken) { - updateBrokenConnection(); - } if (isRefreshConnectionFlow && onRefreshComplete) { + if (isFeedConnectionBroken) { + updateBrokenConnection(); + } onRefreshComplete(); return; } if (isFeedConnectionBroken) { + updateBrokenConnection(); Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); return; } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index ed2561e668a00..0fbe27086a2e3 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -130,14 +130,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn if (feed) { if (!isFeedExpired) { customWindow?.close(); - if (isFeedConnectionBroken) { - updateBrokenConnection(); - } if (isRefreshConnectionFlow && onRefreshComplete) { + if (isFeedConnectionBroken) { + updateBrokenConnection(); + } onRefreshComplete(); return; } if (isFeedConnectionBroken) { + updateBrokenConnection(); Navigation.closeRHPFlow(); return; } From 4d718585ba4623977a4991853894bc8386a32829 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 21:02:17 +0000 Subject: [PATCH 10/23] Refactor Remove deprecated canBeMissing --- .../workspace/companyCards/RefreshCardFeedConnectionPage.tsx | 2 +- .../companyCards/WorkspaceCompanyCardsSettingsPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx index 2dfc265b2c3d2..0f9db798180bf 100644 --- a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -26,7 +26,7 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio const {translate} = useLocalize(); - const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true}); + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const currentStep = assignCard?.currentStep; const [isRefreshComplete, setIsRefreshComplete] = useState(false); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx index 48632d29a7cfb..5a8d42a4ab811 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsSettingsPage.tsx @@ -68,7 +68,7 @@ function WorkspaceCompanyCardsSettingsPage({ const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeedData); const isPending = !!selectedFeedData?.pending; const {currencyList} = useCurrencyListState(); - const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY, {canBeMissing: false}); + const [countryByIp] = useOnyx(ONYXKEYS.COUNTRY); const isDirectCardFeed = isDirectFeed(feed); const statementCloseDate = useMemo(() => { if (!selectedFeedData?.statementPeriodEndDay) { From 42ac00305690a27b769148b0a67b14037f9f0aa6 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 22:25:40 +0000 Subject: [PATCH 11/23] Add useBankConnection hook and openBankConnection utility for managing bank connection flows --- .../openBankConnection/index.native.ts | 6 + .../BankConnection/useBankConnection.ts | 148 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts create mode 100644 src/pages/workspace/companyCards/BankConnection/useBankConnection.ts diff --git a/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts b/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts new file mode 100644 index 0000000000000..c1b01ec34d4af --- /dev/null +++ b/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts @@ -0,0 +1,6 @@ +const handleOpenBankConnectionFlow = (url: string) => { + // No-op for native + return null; +}; + +export default handleOpenBankConnectionFlow; diff --git a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts new file mode 100644 index 0000000000000..cdbb26270d13d --- /dev/null +++ b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts @@ -0,0 +1,148 @@ +import {useCallback, useEffect, useState} from 'react'; +import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; +import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; +import useNetwork from '@hooks/useNetwork'; +import Navigation from '@libs/Navigation/Navigation'; +import ROUTES from '@src/ROUTES'; +import CONST from '@src/CONST'; +import type {CombinedCardFeeds, CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import openBankConnection from './openBankConnection'; + +type UseBankConnectionProps = { + policyID?: string; + feed?: CompanyCardFeedWithDomainID; + isPlaid?: boolean; + url?: string | null; + isNewFeedConnected?: boolean; + newFeed?: CompanyCardFeedWithDomainID; + isFeedExpired?: boolean; + isNewFeedHasError?: boolean; + onSuccess?: (newFeed?: CompanyCardFeedWithDomainID) => void; + onFailure?: () => void; + onBackButtonPress?: () => void; + cardFeeds?: CombinedCardFeeds; + shouldOpenWindow?: boolean; +}; + +let customWindow: Window | null = null; + +export default function useBankConnection({ + policyID, + feed, + isPlaid, + url, + isNewFeedConnected, + newFeed, + isFeedExpired, + isNewFeedHasError, + onSuccess, + onFailure, + onBackButtonPress, + cardFeeds, + shouldOpenWindow = true, +}: UseBankConnectionProps) { + const {isOffline} = useNetwork(); + const onImportPlaidAccounts = useImportPlaidAccounts(policyID); + const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false); + + const onOpenBankConnectionFlow = useCallback(() => { + if (!url || !shouldOpenWindow) { + return; + } + customWindow = openBankConnection(url); + }, [url, shouldOpenWindow]); + + const handleBackButtonPress = useCallback(() => { + if (shouldOpenWindow) { + customWindow?.close(); + } + + if (onBackButtonPress) { + onBackButtonPress(); + return; + } + + Navigation.goBack(); + }, [shouldOpenWindow, onBackButtonPress]); + + useEffect(() => { + if (!policyID || !isBlockedToAddNewFeeds || feed) { + return; + } + Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.alias, ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)), { + forceReplace: true, + }); + }, [isBlockedToAddNewFeeds, policyID, feed]); + + useEffect(() => { + const hasConnectionSource = !!url || isPlaid; + const shouldWaitForData = isOffline || isNewFeedHasError || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); + if (!hasConnectionSource || shouldWaitForData) { + return; + } + + // Handle existing feed flow + if (feed) { + if (!isFeedExpired) { + if (shouldOpenWindow) { + customWindow?.close(); + } + const hasBrokenConnection = !!cardFeeds?.[feed]?.errors; + if (hasBrokenConnection) { + onFailure?.(); + return; + } + onSuccess?.(); + return; + } + if (!isPlaid && url && shouldOpenWindow) { + customWindow = openBankConnection(url); + } + return; + } + + // Handle new feed flow + if (isNewFeedConnected) { + setShouldBlockWindowOpen(true); + if (shouldOpenWindow) { + customWindow?.close(); + } + onSuccess?.(newFeed); + return; + } + + if (!shouldBlockWindowOpen) { + if (isPlaid) { + onImportPlaidAccounts(); + return; + } + if (url && shouldOpenWindow) { + customWindow = openBankConnection(url); + } + } + }, [ + isNewFeedConnected, + isAllFeedsResultLoading, + shouldBlockWindowOpen, + isBlockedToAddNewFeeds, + newFeed, + policyID, + url, + feed, + isFeedExpired, + isOffline, + isPlaid, + onImportPlaidAccounts, + isNewFeedHasError, + onSuccess, + onFailure, + cardFeeds, + shouldOpenWindow, + ]); + + return { + onOpenBankConnectionFlow, + handleBackButtonPress, + }; +} From 5f78cb36c3f880b98671507d4a160457d723f7c3 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 22:26:08 +0000 Subject: [PATCH 12/23] Refactor BankConnection component to utilize useBankConnection hook for improved management of bank connection flows and update callback props for success and failure handling. --- .../BankConnection/index.native.tsx | 124 ++++--------- .../companyCards/BankConnection/index.tsx | 165 +++--------------- 2 files changed, 63 insertions(+), 226 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index a1df71c585775..c51fa932a31b8 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useMemo, useRef, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; @@ -7,28 +7,24 @@ import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useCardFeeds from '@hooks/useCardFeeds'; -import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; -import {updateSelectedFeed} from '@libs/actions/Card'; -import {setAssignCardStepAndData} from '@libs/actions/CompanyCards'; import {checkIfNewFeedConnected, getBankName, getCompanyCardFeed, isSelectedFeedExpired} from '@libs/CardUtils'; import getUAForWebView from '@libs/getUAForWebView'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; -import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import {getCompanyCardBankConnection} from '@userActions/getCompanyCardBankConnection'; 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'; +import useBankConnection from './useBankConnection'; type BankConnectionProps = { /** ID of the policy */ @@ -43,11 +39,17 @@ type BankConnectionProps = { /** Whether this is a refresh card list flow */ isRefreshConnectionFlow?: boolean; - /** Called when re-authentication completes successfully in a refresh flow */ - onRefreshComplete?: () => void; + /** Called when the assign flow succeeds */ + onSuccess?: () => void; + + /** Called when the assign flow fails due to broken connection */ + onFailure?: () => void; + + /** Called when the back button is pressed */ + onBackButtonPress?: () => void; }; -function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onRefreshComplete}: BankConnectionProps) { +function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onSuccess, onFailure, onBackButtonPress}: BankConnectionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const webViewRef = useRef(null); @@ -65,105 +67,49 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const [cardFeeds] = useCardFeeds(policyID); const [isConnectionCompleted, setConnectionCompleted] = useState(false); const prevFeedsData = usePrevious(cardFeeds); - const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.[feed]) : false; + const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; const {isNewFeedConnected, newFeed} = useMemo( () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); - const headerTitleAddCards = !backTo ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.addCards') : undefined; - const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; - const onImportPlaidAccounts = useImportPlaidAccounts(policyID); - const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); + const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; + const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const renderLoading = () => ; - useEffect(() => { - if (!policyID || !isBlockedToAddNewFeeds || feed) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.alias, ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)), { - forceReplace: true, - }); - }, [isBlockedToAddNewFeeds, policyID, feed]); - - const handleBackButtonPress = () => { - // Handle assign card flow - if (feed) { - Navigation.goBack(); + const handleAssignFailure = useCallback(() => { + if (onFailure) { + onFailure(); return; } + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + }, [onFailure, policyID]); - // Handle add new card flow - if (backTo) { - Navigation.goBack(backTo); + const handleComplete = useCallback(() => { + if (onSuccess) { + onSuccess(); return; } - setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK}); - }; + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + }, [onSuccess, policyID]); - useEffect(() => { - if ((!url && !isPlaid) || isNewFeedHasError) { - return; - } - - // Handle assign card flow - if (feed && !isFeedExpired) { - if (isRefreshConnectionFlow && onRefreshComplete) { - if (isFeedConnectionBroken) { - updateBrokenConnection(); - } - onRefreshComplete(); - return; - } - if (isFeedConnectionBroken) { - updateBrokenConnection(); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - return; - } - setAssignCardStepAndData({ - currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, - isEditing: false, - }); - return; - } - - // Handle add new card flow - if (isNewFeedConnected) { - if (newFeed) { - updateSelectedFeed(newFeed, policyID); - } - - // Direct feeds (except those added via Plaid) are created with default statement period end date. - // Redirect the user to set a custom date. - if (policyID && !isPlaid) { - setAddNewCompanyCardStepAndData({ - step: CONST.COMPANY_CARDS.STEP.SELECT_DIRECT_STATEMENT_CLOSE_DATE, - }); - } else { - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - } - } - if (isPlaid) { - onImportPlaidAccounts(); - } - }, [ - isNewFeedConnected, - newFeed, + const {handleBackButtonPress} = useBankConnection({ policyID, - url, feed, - isFeedExpired, - assignCard?.cardToAssign?.dateOption, isPlaid, - onImportPlaidAccounts, - isFeedConnectionBroken, - updateBrokenConnection, + url, + isNewFeedConnected: !!isNewFeedConnected, + newFeed, + isFeedExpired, isNewFeedHasError, - isRefreshConnectionFlow, - onRefreshComplete, - ]); + onSuccess: handleComplete, + onFailure: handleAssignFailure, + onBackButtonPress, + cardFeeds, + shouldOpenWindow: false, + }); const checkIfConnectionCompleted = (navState: WebViewNavigation) => { if (!navState.url.includes(ROUTES.BANK_CONNECTION_COMPLETE)) { diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 0fbe27086a2e3..fc8b00d6aacd0 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -7,32 +7,22 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useCardFeeds from '@hooks/useCardFeeds'; -import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; -import {setAssignCardStepAndData} from '@libs/actions/CompanyCards'; import {checkIfNewFeedConnected, getBankName, getCompanyCardFeed, isSelectedFeedExpired} from '@libs/CardUtils'; -import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; -import {updateSelectedFeed} from '@userActions/Card'; -import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import {getCompanyCardBankConnection} from '@userActions/getCompanyCardBankConnection'; 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'; -import openBankConnection from './openBankConnection'; - -let customWindow: Window | null = null; +import useBankConnection from './useBankConnection'; type BankConnectionProps = { /** ID of the policy */ @@ -44,14 +34,20 @@ type BankConnectionProps = { /** Route params for add new card flow */ route?: PlatformStackRouteProp; - /** Whether this is a refresh card list flow — always opens bank connection and dismisses on re-auth */ + /** Whether this is a refresh card list flow */ isRefreshConnectionFlow?: boolean; - /** Called when re-authentication completes successfully in a refresh flow */ - onRefreshComplete?: () => void; + /** Called when the assign flow succeeds */ + onSuccess?: () => void; + + /** Called when the assign flow fails due to broken connection */ + onFailure?: () => void; + + /** Called when the back button is pressed */ + onBackButtonPress?: () => void; }; -function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onRefreshComplete}: BankConnectionProps) { +function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onSuccess, onFailure, onBackButtonPress}: BankConnectionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); @@ -61,58 +57,36 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const [cardFeeds] = useCardFeeds(policyID); const prevFeedsData = usePrevious(cardFeeds); const illustrations = useMemoizedLazyIllustrations(['PendingBank']); - const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false); const selectedBank = addNewCard?.data?.selectedBank; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); const {isNewFeedConnected, newFeed} = useMemo( () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); - const {isOffline} = useNetwork(); const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; - const {updateBrokenConnection, isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const isPlaid = !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); - const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.[feed]) : false; + const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; const headerTitleAddCards = !backTo ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.addCards') : undefined; - const headerTitle = feed ? translate('workspace.companyCards.assignCard') : headerTitleAddCards; + const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); - const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const onOpenBankConnectionFlow = useCallback(() => { - if (!url) { - return; - } - customWindow = openBankConnection(url); - }, [url]); - - useEffect(() => { - if (!policyID || !isBlockedToAddNewFeeds || feed) { - return; - } - Navigation.navigate(ROUTES.WORKSPACE_UPGRADE.getRoute(policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.companyCards.alias, ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)), { - forceReplace: true, - }); - }, [isBlockedToAddNewFeeds, policyID, feed]); - - const handleBackButtonPress = () => { - customWindow?.close(); - - // Handle assign card flow - if (feed) { - Navigation.goBack(); - return; - } - - // Handle add new card flow - if (backTo) { - Navigation.goBack(backTo); - return; - } - setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK}); - }; + const {onOpenBankConnectionFlow, handleBackButtonPress} = useBankConnection({ + policyID, + feed, + isPlaid, + url, + isNewFeedConnected: !!isNewFeedConnected, + newFeed, + isFeedExpired, + isNewFeedHasError, + onSuccess, + onFailure, + onBackButtonPress, + cardFeeds, + }); const CustomSubtitle = ( @@ -121,89 +95,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn ); - useEffect(() => { - if ((!url && !isPlaid) || isOffline || isNewFeedHasError || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed)) { - return; - } - - // Handle assign card flow - if (feed) { - if (!isFeedExpired) { - customWindow?.close(); - if (isRefreshConnectionFlow && onRefreshComplete) { - if (isFeedConnectionBroken) { - updateBrokenConnection(); - } - onRefreshComplete(); - return; - } - if (isFeedConnectionBroken) { - updateBrokenConnection(); - Navigation.closeRHPFlow(); - return; - } - setAssignCardStepAndData({ - currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, - isEditing: false, - }); - return; - } - if (!isPlaid && url) { - customWindow = openBankConnection(url); - } - return; - } - - // Handle add new card flow - if (isNewFeedConnected) { - setShouldBlockWindowOpen(true); - customWindow?.close(); - if (newFeed) { - updateSelectedFeed(newFeed, policyID); - } - - // Direct feeds (except those added via Plaid) are created with default statement period end date. - // Redirect the user to set a custom date. - if (policyID && !isPlaid) { - setAddNewCompanyCardStepAndData({ - step: CONST.COMPANY_CARDS.STEP.SELECT_DIRECT_STATEMENT_CLOSE_DATE, - }); - } else { - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); - } - return; - } - if (!shouldBlockWindowOpen) { - if (isPlaid) { - onImportPlaidAccounts(); - return; - } - if (url) { - customWindow = openBankConnection(url); - } - } - }, [ - isNewFeedConnected, - isAllFeedsResultLoading, - shouldBlockWindowOpen, - isBlockedToAddNewFeeds, - newFeed, - policyID, - url, - feed, - isFeedExpired, - isOffline, - assignCard?.cardToAssign?.dateOption, - isPlaid, - onImportPlaidAccounts, - isFeedConnectionBroken, - updateBrokenConnection, - isNewFeedHasError, - isRefreshConnectionFlow, - onRefreshComplete, - ]); - const getContent = () => { if (isNewFeedHasError) { return ( From 055cf33d19aed29c978ea5574217023c628d814e Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 22:26:41 +0000 Subject: [PATCH 13/23] Enhance card connection pages by integrating useUpdateFeedBrokenConnection hook and updating success/failure handling. Add new callback props for improved navigation and state management in BankConnection and AddNewCardPage components. --- .../BrokenCardFeedConnectionPage.tsx | 37 ++++++++++++++++- .../RefreshCardFeedConnectionPage.tsx | 19 ++++++++- .../companyCards/addNew/AddNewCardPage.tsx | 40 +++++++++++++++++-- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx index 9f0521319dbb7..f65402860deae 100644 --- a/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/BrokenCardFeedConnectionPage.tsx @@ -1,14 +1,18 @@ -import React, {useEffect} from 'react'; +import React, {useCallback, useEffect} from 'react'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; +import Navigation from '@libs/Navigation/Navigation'; 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 {clearAssignCardStepAndData, 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 PlaidConnectionStep from './addNew/PlaidConnectionStep'; import BankConnection from './BankConnection'; @@ -21,9 +25,11 @@ function BrokenCardFeedConnectionPage({route, policy}: BrokenCardFeedConnectionP const policyID = policy?.id; const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const currentStep = assignCard?.currentStep; + const {updateBrokenConnection} = useUpdateFeedBrokenConnection({policyID, feed}); useEffect(() => { return () => { @@ -31,12 +37,39 @@ function BrokenCardFeedConnectionPage({route, policy}: BrokenCardFeedConnectionP }; }, []); + const handleAssignSuccess = useCallback(() => { + setAssignCardStepAndData({ + currentStep: assignCard?.cardToAssign?.dateOption ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.ASSIGNEE, + isEditing: false, + }); + }, [assignCard?.cardToAssign?.dateOption]); + + const handleAssignFailure = useCallback(() => { + updateBrokenConnection(); + if (shouldUseNarrowLayout) { + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + return; + } + Navigation.closeRHPFlow(); + }, [policyID, shouldUseNarrowLayout, updateBrokenConnection]); + + const handleBackButtonPress = useCallback(() => { + if (shouldUseNarrowLayout) { + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + return; + } + Navigation.closeRHPFlow(); + }, [policyID, shouldUseNarrowLayout]); + switch (currentStep) { case CONST.COMPANY_CARD.STEP.BANK_CONNECTION: return ( ); case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx index 0f9db798180bf..485cf3fbd9cef 100644 --- a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -4,6 +4,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; @@ -29,6 +30,7 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const currentStep = assignCard?.currentStep; const [isRefreshComplete, setIsRefreshComplete] = useState(false); + const {updateBrokenConnection} = useUpdateFeedBrokenConnection({policyID, feed}); useEffect(() => { return () => { @@ -40,6 +42,19 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio setIsRefreshComplete(true); }, []); + const handleAssignSuccess = useCallback(() => { + handleRefreshComplete(); + }, [handleRefreshComplete]); + + const handleAssignFailure = useCallback(() => { + updateBrokenConnection(); + handleRefreshComplete(); + }, [handleRefreshComplete, updateBrokenConnection]); + + const handleBackButtonPress = useCallback(() => { + Navigation.dismissModal(); + }, []); + if (isRefreshComplete) { return ( @@ -65,7 +80,9 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio policyID={policyID} feed={feed} isRefreshConnectionFlow - onRefreshComplete={handleRefreshComplete} + onSuccess={handleAssignSuccess} + onFailure={handleAssignFailure} + onBackButtonPress={handleBackButtonPress} /> ); case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: diff --git a/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx b/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx index f416414b6676f..d117d782252a8 100644 --- a/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx +++ b/src/pages/workspace/companyCards/addNew/AddNewCardPage.tsx @@ -1,5 +1,5 @@ import {isActingAsDelegateSelector} from '@selectors/Account'; -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import {View} from 'react-native'; import ConfirmModal from '@components/ConfirmModal'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; @@ -11,16 +11,18 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; +import {updateSelectedFeed} from '@libs/actions/Card'; import {navigateToConciergeChat} from '@libs/actions/Report'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import BankConnection from '@pages/workspace/companyCards/BankConnection'; import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; -import {clearAddNewCardFlow, openPolicyAddCardFeedPage} from '@userActions/CompanyCards'; +import {clearAddNewCardFlow, openPolicyAddCardFeedPage, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import AmexCustomFeed from './AmexCustomFeed'; import CardInstructionsStep from './CardInstructionsStep'; @@ -73,6 +75,32 @@ function AddNewCardPage({policy}: WithPolicyAndFullscreenLoadingProps) { openPolicyAddCardFeedPage(policyID); }, [policyID]); + const handleBankConnectionSuccess = useCallback( + (newFeed?: CompanyCardFeedWithDomainID) => { + if (newFeed) { + updateSelectedFeed(newFeed, policyID); + } + + const isPlaid = !!addNewCardFeed?.data?.publicToken; + + // Direct feeds (except those added via Plaid) are created with default statement period end date. + // Redirect the user to set a custom date. + if (policyID && !isPlaid) { + setAddNewCompanyCardStepAndData({ + step: CONST.COMPANY_CARDS.STEP.SELECT_DIRECT_STATEMENT_CLOSE_DATE, + }); + } else { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + } + }, + [addNewCardFeed?.data?.publicToken, policyID], + ); + + const handleBackButtonPress = useCallback(() => { + setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK}); + }, []); + if (isAddCardFeedLoading || isAllFeedsResultLoading || isBlockedToAddNewFeeds) { return ; } @@ -101,7 +129,13 @@ function AddNewCardPage({policy}: WithPolicyAndFullscreenLoadingProps) { CurrentStep = ; break; case CONST.COMPANY_CARDS.STEP.BANK_CONNECTION: - CurrentStep = ; + CurrentStep = ( + + ); break; case CONST.COMPANY_CARDS.STEP.CARD_INSTRUCTIONS: CurrentStep = ; From 431766958b6ab0a966a5f6113d4af4b28534db18 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 22:41:44 +0000 Subject: [PATCH 14/23] Refactor RefreshCardFeedConnection tests to rename callback from onRefreshComplete to onSuccess for clarity and consistency. Update related test assertions and mock implementations accordingly. --- tests/ui/RefreshCardFeedConnection.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/ui/RefreshCardFeedConnection.tsx b/tests/ui/RefreshCardFeedConnection.tsx index 0fcde0a1dd983..52bfef167357a 100644 --- a/tests/ui/RefreshCardFeedConnection.tsx +++ b/tests/ui/RefreshCardFeedConnection.tsx @@ -63,18 +63,18 @@ jest.mock('@userActions/CompanyCards', () => ({ setAddNewCompanyCardStepAndData: jest.fn(), })); -let capturedOnRefreshComplete: (() => void) | undefined; +let capturedOnSuccess: (() => void) | undefined; jest.mock('@pages/workspace/companyCards/BankConnection', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const {View, Text} = require('react-native'); return { __esModule: true, // eslint-disable-next-line @typescript-eslint/no-unsafe-return - default: ({onRefreshComplete, isRefreshConnectionFlow}: {onRefreshComplete?: () => void; isRefreshConnectionFlow?: boolean}) => { - capturedOnRefreshComplete = onRefreshComplete; + default: ({onSuccess}: {onSuccess?: () => void}) => { + capturedOnSuccess = onSuccess; return ( - {isRefreshConnectionFlow ? 'refresh-mode' : 'normal-mode'} + {onSuccess ? 'has-refresh-callback' : 'no-refresh-callback'} ); }, @@ -119,7 +119,7 @@ describe('RefreshCardFeedConnection', () => { }); beforeEach(() => { - capturedOnRefreshComplete = undefined; + capturedOnSuccess = undefined; jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ isSmallScreenWidth: false, shouldUseNarrowLayout: false, @@ -171,7 +171,7 @@ describe('RefreshCardFeedConnection', () => { await waitFor(() => { expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); - expect(screen.getByText('refresh-mode')).toBeOnTheScreen(); + expect(screen.getByText('has-refresh-callback')).toBeOnTheScreen(); }); unmount(); @@ -202,7 +202,7 @@ describe('RefreshCardFeedConnection', () => { }); describe('Success view', () => { - it('should show success confirmation when onRefreshComplete is called', async () => { + it('should show success confirmation when onSuccess is called', async () => { await TestHelper.signInWithTestUser(); const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; @@ -220,11 +220,11 @@ describe('RefreshCardFeedConnection', () => { expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); }); - expect(capturedOnRefreshComplete).toBeDefined(); + expect(capturedOnSuccess).toBeDefined(); - // Simulate BankConnection calling onRefreshComplete after successful re-auth + // Simulate BankConnection calling onSuccess after successful re-auth act(() => { - capturedOnRefreshComplete?.(); + capturedOnSuccess?.(); }); await waitFor(() => { @@ -255,7 +255,7 @@ describe('RefreshCardFeedConnection', () => { // Trigger the success view act(() => { - capturedOnRefreshComplete?.(); + capturedOnSuccess?.(); }); await waitFor(() => { @@ -286,7 +286,7 @@ describe('RefreshCardFeedConnection', () => { await waitForBatchedUpdatesWithAct(); act(() => { - capturedOnRefreshComplete?.(); + capturedOnSuccess?.(); }); await waitFor(() => { From c33f43f323eb84648aaf07dfde5cf90d7d88961f Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 22:55:02 +0000 Subject: [PATCH 15/23] Refactor useBankConnection hook to replace state with useRef for shouldBlockWindowOpen, improving performance and reducing unnecessary re-renders. Update openBankConnection utility to remove unused parameter. --- .../BankConnection/openBankConnection/index.native.ts | 2 +- .../companyCards/BankConnection/useBankConnection.ts | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts b/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts index c1b01ec34d4af..2b0b42fa3958e 100644 --- a/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts +++ b/src/pages/workspace/companyCards/BankConnection/openBankConnection/index.native.ts @@ -1,4 +1,4 @@ -const handleOpenBankConnectionFlow = (url: string) => { +const handleOpenBankConnectionFlow = () => { // No-op for native return null; }; diff --git a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts index cdbb26270d13d..5efd979bf9150 100644 --- a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts +++ b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useState} from 'react'; +import {useCallback, useEffect, useRef} from 'react'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useNetwork from '@hooks/useNetwork'; @@ -44,7 +44,7 @@ export default function useBankConnection({ const {isOffline} = useNetwork(); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false); + const shouldBlockWindowOpen = useRef(false); const onOpenBankConnectionFlow = useCallback(() => { if (!url || !shouldOpenWindow) { @@ -77,7 +77,7 @@ export default function useBankConnection({ useEffect(() => { const hasConnectionSource = !!url || isPlaid; - const shouldWaitForData = isOffline || isNewFeedHasError || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); + const shouldWaitForData = isOffline || (isNewFeedHasError ?? false) || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); if (!hasConnectionSource || shouldWaitForData) { return; } @@ -104,7 +104,7 @@ export default function useBankConnection({ // Handle new feed flow if (isNewFeedConnected) { - setShouldBlockWindowOpen(true); + shouldBlockWindowOpen.current = true; if (shouldOpenWindow) { customWindow?.close(); } @@ -112,7 +112,7 @@ export default function useBankConnection({ return; } - if (!shouldBlockWindowOpen) { + if (!shouldBlockWindowOpen.current) { if (isPlaid) { onImportPlaidAccounts(); return; @@ -124,7 +124,6 @@ export default function useBankConnection({ }, [ isNewFeedConnected, isAllFeedsResultLoading, - shouldBlockWindowOpen, isBlockedToAddNewFeeds, newFeed, policyID, From 479b93f4bfb163ae800b60e63a2c4c29f60f055c Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Mon, 9 Mar 2026 23:48:35 +0000 Subject: [PATCH 16/23] Refactor BankConnection and RefreshCardFeedConnectionPage components to improve success and failure handling. Integrate useUpdateFeedBrokenConnection hook for better state management and update callback props for navigation consistency. --- .../BankConnection/index.native.tsx | 23 ++---------- .../BankConnection/useBankConnection.ts | 37 ++++++++++++++----- .../RefreshCardFeedConnectionPage.tsx | 2 + 3 files changed, 33 insertions(+), 29 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index c51fa932a31b8..d0e70d3df6c9e 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo, useRef, useState} from 'react'; +import React, {useMemo, useRef, useState} from 'react'; import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; @@ -14,7 +14,6 @@ import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import {checkIfNewFeedConnected, getBankName, getCompanyCardFeed, isSelectedFeedExpired} from '@libs/CardUtils'; import getUAForWebView from '@libs/getUAForWebView'; -import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; @@ -79,22 +78,6 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const renderLoading = () => ; - const handleAssignFailure = useCallback(() => { - if (onFailure) { - onFailure(); - return; - } - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - }, [onFailure, policyID]); - - const handleComplete = useCallback(() => { - if (onSuccess) { - onSuccess(); - return; - } - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - }, [onSuccess, policyID]); - const {handleBackButtonPress} = useBankConnection({ policyID, feed, @@ -104,8 +87,8 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn newFeed, isFeedExpired, isNewFeedHasError, - onSuccess: handleComplete, - onFailure: handleAssignFailure, + onSuccess, + onFailure, onBackButtonPress, cardFeeds, shouldOpenWindow: false, diff --git a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts index 5efd979bf9150..8becfd4f8ac34 100644 --- a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts +++ b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts @@ -1,10 +1,11 @@ -import {useCallback, useEffect, useRef} from 'react'; +import {useCallback, useEffect, useMemo, useRef} from 'react'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useNetwork from '@hooks/useNetwork'; +import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; import Navigation from '@libs/Navigation/Navigation'; -import ROUTES from '@src/ROUTES'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; import type {CombinedCardFeeds, CompanyCardFeedWithDomainID} from '@src/types/onyx'; import openBankConnection from './openBankConnection'; @@ -44,8 +45,26 @@ export default function useBankConnection({ const {isOffline} = useNetwork(); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); + const {isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const shouldBlockWindowOpen = useRef(false); + const fallbackNavigation = useCallback(() => { + Navigation.goBack(policyID ? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID) : undefined); + }, [policyID]); + + const handleSuccess = useCallback( + (connectedFeed?: CompanyCardFeedWithDomainID) => { + if (onSuccess) { + onSuccess(connectedFeed); + return; + } + fallbackNavigation(); + }, + [onSuccess, fallbackNavigation], + ); + + const handleFailure = useMemo(() => onFailure ?? fallbackNavigation, [onFailure, fallbackNavigation]); + const onOpenBankConnectionFlow = useCallback(() => { if (!url || !shouldOpenWindow) { return; @@ -88,12 +107,11 @@ export default function useBankConnection({ if (shouldOpenWindow) { customWindow?.close(); } - const hasBrokenConnection = !!cardFeeds?.[feed]?.errors; - if (hasBrokenConnection) { - onFailure?.(); + if (isFeedConnectionBroken) { + handleFailure(); return; } - onSuccess?.(); + handleSuccess(); return; } if (!isPlaid && url && shouldOpenWindow) { @@ -108,7 +126,7 @@ export default function useBankConnection({ if (shouldOpenWindow) { customWindow?.close(); } - onSuccess?.(newFeed); + handleSuccess(newFeed); return; } @@ -134,8 +152,9 @@ export default function useBankConnection({ isPlaid, onImportPlaidAccounts, isNewFeedHasError, - onSuccess, - onFailure, + isFeedConnectionBroken, + handleSuccess, + handleFailure, cardFeeds, shouldOpenWindow, ]); diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx index 485cf3fbd9cef..5748f673d8068 100644 --- a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -48,6 +48,8 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio const handleAssignFailure = useCallback(() => { updateBrokenConnection(); + // Re-auth itself succeeded (user completed bank login), so we show the same + // confirmation. The backend will re-scrape with the updated credentials. handleRefreshComplete(); }, [handleRefreshComplete, updateBrokenConnection]); From 3f3e143636b10bdc4af6fa597957af812a512e73 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Tue, 10 Mar 2026 00:24:34 +0000 Subject: [PATCH 17/23] Refactor BankConnection component to streamline state management and improve readability. Remove unused hooks and variables, and enhance the useBankConnection hook for better handling of bank connection logic. --- .../BankConnection/index.native.tsx | 42 +++------------ .../companyCards/BankConnection/index.tsx | 54 +++++-------------- .../BankConnection/useBankConnection.ts | 52 ++++++++++++------ 3 files changed, 56 insertions(+), 92 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index d0e70d3df6c9e..c37c05d5697e1 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -1,4 +1,4 @@ -import React, {useMemo, useRef, useState} from 'react'; +import React, {useRef, useState} from 'react'; import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; @@ -6,18 +6,13 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import useCardFeeds from '@hooks/useCardFeeds'; -import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import {checkIfNewFeedConnected, getBankName, getCompanyCardFeed, isSelectedFeedExpired} from '@libs/CardUtils'; import getUAForWebView from '@libs/getUAForWebView'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; -import {getCompanyCardBankConnection} from '@userActions/getCompanyCardBankConnection'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -53,47 +48,26 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const {translate} = useLocalize(); const webViewRef = useRef(null); const [session] = useOnyx(ONYXKEYS.SESSION); - const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const authToken = session?.authToken ?? null; - const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const selectedBank = addNewCard?.data?.selectedBank; const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; - const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); - const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; - const isPlaid = !!plaidToken; - const url = getCompanyCardBankConnection(policyID, bankName); - const [cardFeeds] = useCardFeeds(policyID); const [isConnectionCompleted, setConnectionCompleted] = useState(false); - const prevFeedsData = usePrevious(cardFeeds); - const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; - const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], - ); - const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; - const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; - const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); - const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - - const renderLoading = () => ; - const {handleBackButtonPress} = useBankConnection({ + const {handleBackButtonPress, url, isPlaid, isNewFeedHasError, newFeed, isAllFeedsResultLoading, isBlockedToAddNewFeeds} = useBankConnection({ policyID, feed, - isPlaid, - url, - isNewFeedConnected: !!isNewFeedConnected, - newFeed, - isFeedExpired, - isNewFeedHasError, + bankNameFromRoute, onSuccess, onFailure, onBackButtonPress, - cardFeeds, shouldOpenWindow: false, }); + const headerTitleAddCards = !backTo ? translate('workspace.companyCards.addCards') : undefined; + const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; + + const renderLoading = () => ; + const checkIfConnectionCompleted = (navState: WebViewNavigation) => { if (!navState.url.includes(ROUTES.BANK_CONNECTION_COMPLETE)) { return; diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index fc8b00d6aacd0..3c21daa86a8bf 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; @@ -6,20 +6,13 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; -import useCardFeeds from '@hooks/useCardFeeds'; -import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; -import {checkIfNewFeedConnected, getBankName, getCompanyCardFeed, isSelectedFeedExpired} from '@libs/CardUtils'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; -import {getCompanyCardBankConnection} from '@userActions/getCompanyCardBankConnection'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import useBankConnection from './useBankConnection'; @@ -50,47 +43,26 @@ type BankConnectionProps = { function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConnectionFlow, onSuccess, onFailure, onBackButtonPress}: BankConnectionProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + const illustrations = useMemoizedLazyIllustrations(['PendingBank']); const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; - const [cardFeeds] = useCardFeeds(policyID); - const prevFeedsData = usePrevious(cardFeeds); - const illustrations = useMemoizedLazyIllustrations(['PendingBank']); - const selectedBank = addNewCard?.data?.selectedBank; - const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); - const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], - ); - const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; - const isPlaid = !!plaidToken; - const url = getCompanyCardBankConnection(policyID, bankName); - const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; + const {onOpenBankConnectionFlow, handleBackButtonPress, bankName, bankDisplayName, isPlaid, isNewFeedHasError, newFeed, isAllFeedsResultLoading, isBlockedToAddNewFeeds} = + useBankConnection({ + policyID, + feed, + bankNameFromRoute, + onSuccess, + onFailure, + onBackButtonPress, + }); + const headerTitleAddCards = !backTo ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; - const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); - const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); - - const {onOpenBankConnectionFlow, handleBackButtonPress} = useBankConnection({ - policyID, - feed, - isPlaid, - url, - isNewFeedConnected: !!isNewFeedConnected, - newFeed, - isFeedExpired, - isNewFeedHasError, - onSuccess, - onFailure, - onBackButtonPress, - cardFeeds, - }); const CustomSubtitle = ( - {bankName && translate(`workspace.moreFeatures.companyCards.pendingBankDescription`, addNewCard?.data?.plaidConnectedFeedName ?? bankName)} + {bankName && translate(`workspace.moreFeatures.companyCards.pendingBankDescription`, bankDisplayName ?? bankName)} {translate('workspace.moreFeatures.companyCards.pendingBankLink')}. ); diff --git a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts index 8becfd4f8ac34..79d054cdd46c7 100644 --- a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts +++ b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts @@ -1,27 +1,27 @@ import {useCallback, useEffect, useMemo, useRef} from 'react'; +import useCardFeeds from '@hooks/useCardFeeds'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePrevious from '@hooks/usePrevious'; import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; +import {checkIfNewFeedConnected, getBankName, getCompanyCardFeed, isSelectedFeedExpired} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; +import {getCompanyCardBankConnection} from '@userActions/getCompanyCardBankConnection'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {CombinedCardFeeds, CompanyCardFeedWithDomainID} from '@src/types/onyx'; +import type {CompanyCardFeedWithDomainID} from '@src/types/onyx'; import openBankConnection from './openBankConnection'; type UseBankConnectionProps = { policyID?: string; feed?: CompanyCardFeedWithDomainID; - isPlaid?: boolean; - url?: string | null; - isNewFeedConnected?: boolean; - newFeed?: CompanyCardFeedWithDomainID; - isFeedExpired?: boolean; - isNewFeedHasError?: boolean; + bankNameFromRoute?: string | null; onSuccess?: (newFeed?: CompanyCardFeedWithDomainID) => void; onFailure?: () => void; onBackButtonPress?: () => void; - cardFeeds?: CombinedCardFeeds; shouldOpenWindow?: boolean; }; @@ -30,24 +30,35 @@ let customWindow: Window | null = null; export default function useBankConnection({ policyID, feed, - isPlaid, - url, - isNewFeedConnected, - newFeed, - isFeedExpired, - isNewFeedHasError, + bankNameFromRoute, onSuccess, onFailure, onBackButtonPress, - cardFeeds, shouldOpenWindow = true, }: UseBankConnectionProps) { const {isOffline} = useNetwork(); + const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + const [cardFeeds] = useCardFeeds(policyID); + const prevFeedsData = usePrevious(cardFeeds); const onImportPlaidAccounts = useImportPlaidAccounts(policyID); const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const {isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const shouldBlockWindowOpen = useRef(false); + const selectedBank = addNewCard?.data?.selectedBank; + const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); + const bankDisplayName = addNewCard?.data?.plaidConnectedFeedName ?? bankName; + const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; + const isPlaid = !!plaidToken; + const url = getCompanyCardBankConnection(policyID, bankName); + const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; + const {isNewFeedConnected, newFeed} = useMemo( + () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), + [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], + ); + const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); + const fallbackNavigation = useCallback(() => { Navigation.goBack(policyID ? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID) : undefined); }, [policyID]); @@ -96,7 +107,7 @@ export default function useBankConnection({ useEffect(() => { const hasConnectionSource = !!url || isPlaid; - const shouldWaitForData = isOffline || (isNewFeedHasError ?? false) || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); + const shouldWaitForData = isOffline || isNewFeedHasError || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); if (!hasConnectionSource || shouldWaitForData) { return; } @@ -155,12 +166,19 @@ export default function useBankConnection({ isFeedConnectionBroken, handleSuccess, handleFailure, - cardFeeds, shouldOpenWindow, ]); return { onOpenBankConnectionFlow, handleBackButtonPress, + bankName, + bankDisplayName, + url, + isPlaid, + isNewFeedHasError, + newFeed, + isAllFeedsResultLoading, + isBlockedToAddNewFeeds, }; } From 9efbcd4da4384240b96633bfb9ecaa17b3252712 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Tue, 10 Mar 2026 01:08:54 +0000 Subject: [PATCH 18/23] Move refresh success confirmation into BankConnection and derive completion state Shift the refresh-complete confirmation UI from RefreshCardFeedConnectionPage into BankConnection (both web and native) so the component owns the full flow lifecycle. Replace imperative isRefreshComplete useState with a derived useMemo to eliminate the setState-in-effect lint warning. Simplify RefreshCardFeedConnectionPage by removing success/failure callbacks and navigating back to feed settings on back press. Update tests to match the new component API. --- .../BankConnection/index.native.tsx | 84 ++++++++++------- .../companyCards/BankConnection/index.tsx | 44 +++++++-- .../BankConnection/useBankConnection.ts | 13 ++- .../RefreshCardFeedConnectionPage.tsx | 47 +--------- tests/ui/RefreshCardFeedConnection.tsx | 92 +++---------------- 5 files changed, 113 insertions(+), 167 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index c37c05d5697e1..0bb18a705eebf 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -3,6 +3,7 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmationPage from '@components/ConfirmationPage'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -10,6 +11,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import getUAForWebView from '@libs/getUAForWebView'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; @@ -53,13 +55,14 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const policyID = policyIDFromProps ?? policyIDFromRoute; const [isConnectionCompleted, setConnectionCompleted] = useState(false); - const {handleBackButtonPress, url, isPlaid, isNewFeedHasError, newFeed, isAllFeedsResultLoading, isBlockedToAddNewFeeds} = useBankConnection({ + const {handleBackButtonPress, url, isPlaid, isNewFeedHasError, newFeed, isAllFeedsResultLoading, isBlockedToAddNewFeeds, isRefreshComplete} = useBankConnection({ policyID, feed, bankNameFromRoute, onSuccess, onFailure, onBackButtonPress, + isRefreshConnectionFlow, shouldOpenWindow: false, }); @@ -75,9 +78,55 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn setConnectionCompleted(true); }; + const getContent = () => { + if (isRefreshComplete) { + return ( + Navigation.dismissModal()} + /> + ); + } + if (isNewFeedHasError) { + return ( + + ); + } + if (!!url && !isConnectionCompleted && !isPlaid && !isAllFeedsResultLoading && (!isBlockedToAddNewFeeds || !!feed)) { + return ( + + ); + } + return ( + + ); + }; + return ( - - {!!url && !isConnectionCompleted && !isPlaid && !isNewFeedHasError && !isAllFeedsResultLoading && (!isBlockedToAddNewFeeds || !!feed) && ( - - )} - {(isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed) || isConnectionCompleted || isPlaid) && !isNewFeedHasError && ( - - )} - {isNewFeedHasError && ( - - )} - + {getContent()} ); } diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 3c21daa86a8bf..f6b9303d7fa0f 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,6 +1,7 @@ import React from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; +import ConfirmationPage from '@components/ConfirmationPage'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -9,6 +10,7 @@ import TextLink from '@components/TextLink'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; import WorkspaceCompanyCardsErrorConfirmation from '@pages/workspace/companyCards/WorkspaceCompanyCardsErrorConfirmation'; @@ -47,15 +49,26 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn const {feed: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; const policyID = policyIDFromProps ?? policyIDFromRoute; - const {onOpenBankConnectionFlow, handleBackButtonPress, bankName, bankDisplayName, isPlaid, isNewFeedHasError, newFeed, isAllFeedsResultLoading, isBlockedToAddNewFeeds} = - useBankConnection({ - policyID, - feed, - bankNameFromRoute, - onSuccess, - onFailure, - onBackButtonPress, - }); + const { + onOpenBankConnectionFlow, + handleBackButtonPress, + bankName, + bankDisplayName, + isPlaid, + isNewFeedHasError, + newFeed, + isAllFeedsResultLoading, + isBlockedToAddNewFeeds, + isRefreshComplete, + } = useBankConnection({ + policyID, + feed, + bankNameFromRoute, + onSuccess, + onFailure, + onBackButtonPress, + isRefreshConnectionFlow, + }); const headerTitleAddCards = !backTo ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.addCards') : undefined; const headerTitle = feed ? translate(isRefreshConnectionFlow ? 'workspace.moreFeatures.companyCards.assignNewCards' : 'workspace.companyCards.assignCard') : headerTitleAddCards; @@ -68,6 +81,17 @@ function BankConnection({policyID: policyIDFromProps, feed, route, isRefreshConn ); const getContent = () => { + if (isRefreshComplete) { + return ( + Navigation.dismissModal()} + /> + ); + } if (isNewFeedHasError) { return ( void; onFailure?: () => void; onBackButtonPress?: () => void; + isRefreshConnectionFlow?: boolean; shouldOpenWindow?: boolean; }; @@ -34,6 +35,7 @@ export default function useBankConnection({ onSuccess, onFailure, onBackButtonPress, + isRefreshConnectionFlow, shouldOpenWindow = true, }: UseBankConnectionProps) { const {isOffline} = useNetwork(); @@ -45,6 +47,7 @@ export default function useBankConnection({ const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const {isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const shouldBlockWindowOpen = useRef(false); + const [isRefreshComplete, setIsRefreshComplete] = useState(false); const selectedBank = addNewCard?.data?.selectedBank; const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); @@ -65,13 +68,18 @@ export default function useBankConnection({ const handleSuccess = useCallback( (connectedFeed?: CompanyCardFeedWithDomainID) => { + if (isRefreshConnectionFlow) { + onSuccess?.(connectedFeed ?? undefined); + setIsRefreshComplete(true); + return; + } if (onSuccess) { onSuccess(connectedFeed); return; } fallbackNavigation(); }, - [onSuccess, fallbackNavigation], + [onSuccess, fallbackNavigation, isRefreshConnectionFlow], ); const handleFailure = useMemo(() => onFailure ?? fallbackNavigation, [onFailure, fallbackNavigation]); @@ -180,5 +188,6 @@ export default function useBankConnection({ newFeed, isAllFeedsResultLoading, isBlockedToAddNewFeeds, + isRefreshComplete, }; } diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx index 5748f673d8068..515e2e952d88e 100644 --- a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -1,7 +1,4 @@ -import React, {useCallback, useEffect, useState} from 'react'; -import ConfirmationPage from '@components/ConfirmationPage'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; +import React, {useCallback, useEffect} from 'react'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useUpdateFeedBrokenConnection from '@hooks/useUpdateFeedBrokenConnection'; @@ -14,6 +11,7 @@ import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullsc import {clearAssignCardStepAndData} 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 PlaidConnectionStep from './addNew/PlaidConnectionStep'; import BankConnection from './BankConnection'; @@ -29,7 +27,6 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); const currentStep = assignCard?.currentStep; - const [isRefreshComplete, setIsRefreshComplete] = useState(false); const {updateBrokenConnection} = useUpdateFeedBrokenConnection({policyID, feed}); useEffect(() => { @@ -38,42 +35,9 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio }; }, []); - const handleRefreshComplete = useCallback(() => { - setIsRefreshComplete(true); - }, []); - - const handleAssignSuccess = useCallback(() => { - handleRefreshComplete(); - }, [handleRefreshComplete]); - - const handleAssignFailure = useCallback(() => { - updateBrokenConnection(); - // Re-auth itself succeeded (user completed bank login), so we show the same - // confirmation. The backend will re-scrape with the updated credentials. - handleRefreshComplete(); - }, [handleRefreshComplete, updateBrokenConnection]); - const handleBackButtonPress = useCallback(() => { - Navigation.dismissModal(); - }, []); - - if (isRefreshComplete) { - return ( - - Navigation.dismissModal()} - /> - Navigation.dismissModal()} - /> - - ); - } + Navigation.goBack(policyID ? ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID) : undefined); + }, [policyID]); switch (currentStep) { case CONST.COMPANY_CARD.STEP.BANK_CONNECTION: @@ -82,8 +46,7 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio policyID={policyID} feed={feed} isRefreshConnectionFlow - onSuccess={handleAssignSuccess} - onFailure={handleAssignFailure} + onSuccess={updateBrokenConnection} onBackButtonPress={handleBackButtonPress} /> ); diff --git a/tests/ui/RefreshCardFeedConnection.tsx b/tests/ui/RefreshCardFeedConnection.tsx index 52bfef167357a..d1c30cd5b4b3e 100644 --- a/tests/ui/RefreshCardFeedConnection.tsx +++ b/tests/ui/RefreshCardFeedConnection.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/naming-convention */ import {PortalProvider} from '@gorhom/portal'; import {NavigationContainer} from '@react-navigation/native'; -import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; +import {act, render, screen, waitFor} from '@testing-library/react-native'; import React from 'react'; import Onyx from 'react-native-onyx'; import ComposeProviders from '@components/ComposeProviders'; @@ -63,18 +63,18 @@ jest.mock('@userActions/CompanyCards', () => ({ setAddNewCompanyCardStepAndData: jest.fn(), })); -let capturedOnSuccess: (() => void) | undefined; +let capturedOnFailure: (() => void) | undefined; jest.mock('@pages/workspace/companyCards/BankConnection', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const {View, Text} = require('react-native'); return { __esModule: true, // eslint-disable-next-line @typescript-eslint/no-unsafe-return - default: ({onSuccess}: {onSuccess?: () => void}) => { - capturedOnSuccess = onSuccess; + default: ({isRefreshConnectionFlow, onFailure}: {isRefreshConnectionFlow?: boolean; onFailure?: () => void}) => { + capturedOnFailure = onFailure; return ( - {onSuccess ? 'has-refresh-callback' : 'no-refresh-callback'} + {isRefreshConnectionFlow ? 'refresh-flow' : 'normal-flow'} ); }, @@ -119,7 +119,7 @@ describe('RefreshCardFeedConnection', () => { }); beforeEach(() => { - capturedOnSuccess = undefined; + capturedOnFailure = undefined; jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ isSmallScreenWidth: false, shouldUseNarrowLayout: false, @@ -171,7 +171,7 @@ describe('RefreshCardFeedConnection', () => { await waitFor(() => { expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); - expect(screen.getByText('has-refresh-callback')).toBeOnTheScreen(); + expect(screen.getByText('refresh-flow')).toBeOnTheScreen(); }); unmount(); @@ -201,8 +201,8 @@ describe('RefreshCardFeedConnection', () => { }); }); - describe('Success view', () => { - it('should show success confirmation when onSuccess is called', async () => { + describe('Failure handling', () => { + it('should dismiss modal when onFailure is called', async () => { await TestHelper.signInWithTestUser(); const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; @@ -220,82 +220,12 @@ describe('RefreshCardFeedConnection', () => { expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); }); - expect(capturedOnSuccess).toBeDefined(); + expect(capturedOnFailure).toBeDefined(); - // Simulate BankConnection calling onSuccess after successful re-auth act(() => { - capturedOnSuccess?.(); + capturedOnFailure?.(); }); - await waitFor(() => { - expect(screen.getByText('Connection refreshed')).toBeOnTheScreen(); - expect(screen.getByTestId('confirmation-primary-button')).toBeOnTheScreen(); - }); - - // BankConnection should no longer be rendered - expect(screen.queryByTestId('BankConnection')).toBeNull(); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('should dismiss modal when Got it button is pressed', async () => { - await TestHelper.signInWithTestUser(); - - const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; - - await act(async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); - await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); - await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); - }); - - const {unmount} = renderRefreshPage(); - await waitForBatchedUpdatesWithAct(); - - // Trigger the success view - act(() => { - capturedOnSuccess?.(); - }); - - await waitFor(() => { - expect(screen.getByTestId('confirmation-primary-button')).toBeOnTheScreen(); - }); - - // Press the "Got it" button - fireEvent.press(screen.getByTestId('confirmation-primary-button')); - - expect(Navigation.dismissModal).toHaveBeenCalled(); - - unmount(); - await waitForBatchedUpdatesWithAct(); - }); - - it('should dismiss modal when back button is pressed on success view', async () => { - await TestHelper.signInWithTestUser(); - - const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; - - await act(async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy); - await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); - await Onyx.merge(ONYXKEYS.ASSIGN_CARD, {currentStep: CONST.COMPANY_CARD.STEP.BANK_CONNECTION}); - }); - - const {unmount} = renderRefreshPage(); - await waitForBatchedUpdatesWithAct(); - - act(() => { - capturedOnSuccess?.(); - }); - - await waitFor(() => { - expect(screen.getByText('Connection refreshed')).toBeOnTheScreen(); - }); - - const backButton = screen.getByLabelText('Back'); - fireEvent.press(backButton); - expect(Navigation.dismissModal).toHaveBeenCalled(); unmount(); From 6b3abae2065df0d4cb0c6aed025a57aab92724bc Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Tue, 10 Mar 2026 01:11:33 +0000 Subject: [PATCH 19/23] Simplify useBankConnection: derive refresh state, extract shared variables Replace imperative isRefreshComplete useState with a derived useMemo to fix the setState-in-effect lint warning. Hoist hasConnectionSource and shouldWaitForData to the hook top level to eliminate duplication between the useMemo and the main useEffect. Extract closeCustomWindow helper and destructure addNewCardData to reduce repetition. --- .../BankConnection/useBankConnection.ts | 63 ++++++++++++------- 1 file changed, 39 insertions(+), 24 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts index f6d0bdc618fbc..abcc31cc247d1 100644 --- a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts +++ b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts @@ -1,4 +1,4 @@ -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useCallback, useEffect, useMemo, useRef} from 'react'; import useCardFeeds from '@hooks/useCardFeeds'; import useImportPlaidAccounts from '@hooks/useImportPlaidAccounts'; import useIsBlockedToAddFeed from '@hooks/useIsBlockedToAddFeed'; @@ -28,6 +28,10 @@ type UseBankConnectionProps = { let customWindow: Window | null = null; +function closeCustomWindow() { + customWindow?.close(); +} + export default function useBankConnection({ policyID, feed, @@ -47,20 +51,29 @@ export default function useBankConnection({ const {isBlockedToAddNewFeeds, isAllFeedsResultLoading} = useIsBlockedToAddFeed(policyID); const {isFeedConnectionBroken} = useUpdateFeedBrokenConnection({policyID, feed}); const shouldBlockWindowOpen = useRef(false); - const [isRefreshComplete, setIsRefreshComplete] = useState(false); + const refreshSuccessHandled = useRef(false); - const selectedBank = addNewCard?.data?.selectedBank; - const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCard?.data?.plaidConnectedFeed ?? selectedBank); - const bankDisplayName = addNewCard?.data?.plaidConnectedFeedName ?? bankName; - const plaidToken = addNewCard?.data?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; + const addNewCardData = addNewCard?.data; + const bankName = feed ? getBankName(getCompanyCardFeed(feed)) : (bankNameFromRoute ?? addNewCardData?.plaidConnectedFeed ?? addNewCardData?.selectedBank); + const bankDisplayName = addNewCardData?.plaidConnectedFeedName ?? bankName; + const plaidToken = addNewCardData?.publicToken ?? assignCard?.cardToAssign?.plaidAccessToken; const isPlaid = !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; const {isNewFeedConnected, newFeed} = useMemo( - () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCard?.data?.plaidConnectedFeed), - [addNewCard?.data?.plaidConnectedFeed, cardFeeds, prevFeedsData], + () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCardData?.plaidConnectedFeed), + [addNewCardData?.plaidConnectedFeed, cardFeeds, prevFeedsData], ); const isNewFeedHasError = !!(newFeed && cardFeeds?.[newFeed]?.errors); + const hasConnectionSource = !!url || isPlaid; + const shouldWaitForData = isOffline || isNewFeedHasError || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); + + const isRefreshComplete = useMemo(() => { + if (!isRefreshConnectionFlow || !feed || !hasConnectionSource || shouldWaitForData) { + return false; + } + return !isFeedExpired && !isFeedConnectionBroken; + }, [isRefreshConnectionFlow, feed, hasConnectionSource, shouldWaitForData, isFeedExpired, isFeedConnectionBroken]); const fallbackNavigation = useCallback(() => { Navigation.goBack(policyID ? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID) : undefined); @@ -68,18 +81,13 @@ export default function useBankConnection({ const handleSuccess = useCallback( (connectedFeed?: CompanyCardFeedWithDomainID) => { - if (isRefreshConnectionFlow) { - onSuccess?.(connectedFeed ?? undefined); - setIsRefreshComplete(true); - return; - } if (onSuccess) { onSuccess(connectedFeed); return; } fallbackNavigation(); }, - [onSuccess, fallbackNavigation, isRefreshConnectionFlow], + [onSuccess, fallbackNavigation], ); const handleFailure = useMemo(() => onFailure ?? fallbackNavigation, [onFailure, fallbackNavigation]); @@ -93,7 +101,7 @@ export default function useBankConnection({ const handleBackButtonPress = useCallback(() => { if (shouldOpenWindow) { - customWindow?.close(); + closeCustomWindow(); } if (onBackButtonPress) { @@ -104,6 +112,14 @@ export default function useBankConnection({ Navigation.goBack(); }, [shouldOpenWindow, onBackButtonPress]); + useEffect(() => { + if (!isRefreshComplete || refreshSuccessHandled.current) { + return; + } + refreshSuccessHandled.current = true; + onSuccess?.(); + }, [isRefreshComplete, onSuccess]); + useEffect(() => { if (!policyID || !isBlockedToAddNewFeeds || feed) { return; @@ -114,8 +130,6 @@ export default function useBankConnection({ }, [isBlockedToAddNewFeeds, policyID, feed]); useEffect(() => { - const hasConnectionSource = !!url || isPlaid; - const shouldWaitForData = isOffline || isNewFeedHasError || isAllFeedsResultLoading || (isBlockedToAddNewFeeds && !feed); if (!hasConnectionSource || shouldWaitForData) { return; } @@ -124,13 +138,15 @@ export default function useBankConnection({ if (feed) { if (!isFeedExpired) { if (shouldOpenWindow) { - customWindow?.close(); + closeCustomWindow(); } if (isFeedConnectionBroken) { handleFailure(); return; } - handleSuccess(); + if (!isRefreshConnectionFlow) { + handleSuccess(); + } return; } if (!isPlaid && url && shouldOpenWindow) { @@ -143,7 +159,7 @@ export default function useBankConnection({ if (isNewFeedConnected) { shouldBlockWindowOpen.current = true; if (shouldOpenWindow) { - customWindow?.close(); + closeCustomWindow(); } handleSuccess(newFeed); return; @@ -159,22 +175,21 @@ export default function useBankConnection({ } } }, [ + hasConnectionSource, + shouldWaitForData, isNewFeedConnected, - isAllFeedsResultLoading, - isBlockedToAddNewFeeds, newFeed, policyID, url, feed, isFeedExpired, - isOffline, isPlaid, onImportPlaidAccounts, - isNewFeedHasError, isFeedConnectionBroken, handleSuccess, handleFailure, shouldOpenWindow, + isRefreshConnectionFlow, ]); return { From e67ea5a85a3a4d97c8ea0f9d3fb25930d9f8d606 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Tue, 10 Mar 2026 01:32:10 +0000 Subject: [PATCH 20/23] Refactor RefreshCardFeedConnectionPage and BankConnection components to improve navigation and prop handling. Rename back button handler for clarity and update test cases to reflect new prop structure. --- .../BankConnection/index.native.tsx | 2 +- .../companyCards/BankConnection/index.tsx | 2 +- .../RefreshCardFeedConnectionPage.tsx | 6 ++--- tests/ui/RefreshCardFeedConnection.tsx | 23 ++++++++----------- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 0bb18a705eebf..93c9f76a2cf67 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -3,8 +3,8 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import ConfirmationPage from '@components/ConfirmationPage'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index f6b9303d7fa0f..a80dda3ed2dfc 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import ActivityIndicator from '@components/ActivityIndicator'; import BlockingView from '@components/BlockingViews/BlockingView'; -import ConfirmationPage from '@components/ConfirmationPage'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import ConfirmationPage from '@components/ConfirmationPage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; diff --git a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx index 515e2e952d88e..4823433a6e5e1 100644 --- a/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx +++ b/src/pages/workspace/companyCards/RefreshCardFeedConnectionPage.tsx @@ -35,7 +35,7 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio }; }, []); - const handleBackButtonPress = useCallback(() => { + const navigateToFeedSettings = useCallback(() => { Navigation.goBack(policyID ? ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID) : undefined); }, [policyID]); @@ -46,8 +46,8 @@ function RefreshCardFeedConnectionPage({route, policy}: RefreshCardFeedConnectio policyID={policyID} feed={feed} isRefreshConnectionFlow - onSuccess={updateBrokenConnection} - onBackButtonPress={handleBackButtonPress} + onFailure={updateBrokenConnection} + onBackButtonPress={navigateToFeedSettings} /> ); case CONST.COMPANY_CARD.STEP.PLAID_CONNECTION: diff --git a/tests/ui/RefreshCardFeedConnection.tsx b/tests/ui/RefreshCardFeedConnection.tsx index d1c30cd5b4b3e..4a4ad6b6d70c2 100644 --- a/tests/ui/RefreshCardFeedConnection.tsx +++ b/tests/ui/RefreshCardFeedConnection.tsx @@ -63,18 +63,18 @@ jest.mock('@userActions/CompanyCards', () => ({ setAddNewCompanyCardStepAndData: jest.fn(), })); -let capturedOnFailure: (() => void) | undefined; +let capturedProps: {isRefreshConnectionFlow?: boolean; onFailure?: () => void; onBackButtonPress?: () => void} = {}; jest.mock('@pages/workspace/companyCards/BankConnection', () => { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const {View, Text} = require('react-native'); return { __esModule: true, // eslint-disable-next-line @typescript-eslint/no-unsafe-return - default: ({isRefreshConnectionFlow, onFailure}: {isRefreshConnectionFlow?: boolean; onFailure?: () => void}) => { - capturedOnFailure = onFailure; + default: (props: {isRefreshConnectionFlow?: boolean; onFailure?: () => void; onBackButtonPress?: () => void}) => { + capturedProps = props; return ( - {isRefreshConnectionFlow ? 'refresh-flow' : 'normal-flow'} + {props.isRefreshConnectionFlow ? 'refresh-flow' : 'normal-flow'} ); }, @@ -119,7 +119,7 @@ describe('RefreshCardFeedConnection', () => { }); beforeEach(() => { - capturedOnFailure = undefined; + capturedProps = {}; jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({ isSmallScreenWidth: false, shouldUseNarrowLayout: false, @@ -201,8 +201,8 @@ describe('RefreshCardFeedConnection', () => { }); }); - describe('Failure handling', () => { - it('should dismiss modal when onFailure is called', async () => { + describe('BankConnection props', () => { + it('should pass onFailure and onBackButtonPress callbacks', async () => { await TestHelper.signInWithTestUser(); const policy = {...LHNTestUtils.getFakePolicy(), role: CONST.POLICY.ROLE.ADMIN, workspaceAccountID: WORKSPACE_ACCOUNT_ID}; @@ -220,13 +220,8 @@ describe('RefreshCardFeedConnection', () => { expect(screen.getByTestId('BankConnection')).toBeOnTheScreen(); }); - expect(capturedOnFailure).toBeDefined(); - - act(() => { - capturedOnFailure?.(); - }); - - expect(Navigation.dismissModal).toHaveBeenCalled(); + expect(capturedProps.onFailure).toBeDefined(); + expect(capturedProps.onBackButtonPress).toBeDefined(); unmount(); await waitForBatchedUpdatesWithAct(); From dec2e7ff319bc7bafb857db7c6e00796ebba0a12 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Tue, 10 Mar 2026 01:35:16 +0000 Subject: [PATCH 21/23] Add route generation test for RefreshCardFeedConnection --- tests/ui/RefreshCardFeedConnection.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/ui/RefreshCardFeedConnection.tsx b/tests/ui/RefreshCardFeedConnection.tsx index 4a4ad6b6d70c2..a4fa47484c1fc 100644 --- a/tests/ui/RefreshCardFeedConnection.tsx +++ b/tests/ui/RefreshCardFeedConnection.tsx @@ -10,12 +10,12 @@ import OnyxListItemProvider from '@components/OnyxListItemProvider'; import {CurrentReportIDContextProvider} from '@hooks/useCurrentReportID'; import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout'; 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 RefreshCardFeedConnectionPage from '@pages/workspace/companyCards/RefreshCardFeedConnectionPage'; 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'; @@ -248,4 +248,11 @@ describe('RefreshCardFeedConnection', () => { expect(mockClearAssignCardStepAndData).toHaveBeenCalled(); }); }); + + describe('Route generation', () => { + it('should generate correct route for refresh card feed connection', () => { + const route = ROUTES.WORKSPACE_COMPANY_CARDS_REFRESH_CARD_FEED_CONNECTION.getRoute(POLICY_ID, DIRECT_FEED); + expect(route).toBe(`workspaces/${POLICY_ID}/company-cards/${encodeURIComponent(DIRECT_FEED)}/refresh-card-feed-connection`); + }); + }); }); From 1f052dc25807f70698fec4c6bed5589e03d4b31d Mon Sep 17 00:00:00 2001 From: "Fedi Rajhi (via MelvinBot)" Date: Tue, 10 Mar 2026 19:54:11 +0000 Subject: [PATCH 22/23] Fix: Prettier import ordering in BankConnection/index.native.tsx Co-authored-by: Fedi Rajhi --- .../workspace/companyCards/BankConnection/index.native.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index 93c9f76a2cf67..0bb18a705eebf 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -3,8 +3,8 @@ import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; import ActivityIndicator from '@components/ActivityIndicator'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import ConfirmationPage from '@components/ConfirmationPage'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; From 3cf060b5ff5c77071ea42ad0bc7c70cff7504824 Mon Sep 17 00:00:00 2001 From: Fedi Rajhi Date: Thu, 12 Mar 2026 14:38:10 +0000 Subject: [PATCH 23/23] feat: add previous feed expiration state tracking in useBankConnection --- .../companyCards/BankConnection/useBankConnection.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts index abcc31cc247d1..2482a48e04bd7 100644 --- a/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts +++ b/src/pages/workspace/companyCards/BankConnection/useBankConnection.ts @@ -60,6 +60,7 @@ export default function useBankConnection({ const isPlaid = !!plaidToken; const url = getCompanyCardBankConnection(policyID, bankName); const isFeedExpired = feed ? !!isSelectedFeedExpired(cardFeeds?.[feed]) : false; + const prevIsFeedExpired = usePrevious(isFeedExpired); const {isNewFeedConnected, newFeed} = useMemo( () => checkIfNewFeedConnected(prevFeedsData ?? {}, cardFeeds ?? {}, addNewCardData?.plaidConnectedFeed), [addNewCardData?.plaidConnectedFeed, cardFeeds, prevFeedsData], @@ -72,8 +73,8 @@ export default function useBankConnection({ if (!isRefreshConnectionFlow || !feed || !hasConnectionSource || shouldWaitForData) { return false; } - return !isFeedExpired && !isFeedConnectionBroken; - }, [isRefreshConnectionFlow, feed, hasConnectionSource, shouldWaitForData, isFeedExpired, isFeedConnectionBroken]); + return !!prevIsFeedExpired && !isFeedExpired && !isFeedConnectionBroken; + }, [isRefreshConnectionFlow, feed, hasConnectionSource, shouldWaitForData, prevIsFeedExpired, isFeedExpired, isFeedConnectionBroken]); const fallbackNavigation = useCallback(() => { Navigation.goBack(policyID ? ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID) : undefined);