diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b150e6841ec6..eef0990f7409 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1291,6 +1291,15 @@ const ROUTES = { return `settings/workspaces/${policyID}/company-cards` as const; }, }, + WORKSPACE_COMPANY_CARDS_BANK_CONNECTION: { + route: 'settings/workspaces/:policyID/company-cards/:bankName/bank-connection', + getRoute: (policyID: string | undefined, bankName: string, backTo: string) => { + if (!policyID) { + Log.warn('Invalid policyID is used to build the WORKSPACE_COMPANY_CARDS_BANK_CONNECTION route'); + } + return getUrlWithBackToParam(`settings/workspaces/${policyID}/company-cards/${bankName}/bank-connection`, backTo); + }, + }, WORKSPACE_COMPANY_CARDS_ADD_NEW: { route: 'settings/workspaces/:policyID/company-cards/add-card-feed', getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards/add-card-feed` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3d85cd907f2a..76456485a3a4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -456,6 +456,7 @@ const SCREENS = { COMPANY_CARDS: 'Workspace_CompanyCards', COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard', COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed', + COMPANY_CARDS_BANK_CONNECTION: 'Workspace_CompanyCards_BankConnection', COMPANY_CARDS_ADD_NEW: 'Workspace_CompanyCards_New', COMPANY_CARDS_TYPE: 'Workspace_CompanyCards_Type', COMPANY_CARDS_INSTRUCTIONS: 'Workspace_CompanyCards_Instructions', diff --git a/src/components/DotIndicatorMessage.tsx b/src/components/DotIndicatorMessage.tsx index d831fca562c3..c998c38e96ca 100644 --- a/src/components/DotIndicatorMessage.tsx +++ b/src/components/DotIndicatorMessage.tsx @@ -7,8 +7,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {isReceiptError} from '@libs/ErrorUtils'; import fileDownload from '@libs/fileDownload'; -import * as Localize from '@libs/Localize'; -import CONST from '@src/CONST'; +import {translateLocal} from '@libs/Localize'; import type {ReceiptError} from '@src/types/onyx/Transaction'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; @@ -61,38 +60,17 @@ function DotIndicatorMessage({messages = {}, style, type, textStyles}: DotIndica key={index} style={styles.offlineFeedback.text} > - {Localize.translateLocal('iou.error.receiptFailureMessage')} + {translateLocal('iou.error.receiptFailureMessage')} { fileDownload(message.source, message.filename); }} > - {Localize.translateLocal('iou.error.saveFileMessage')} + {translateLocal('iou.error.saveFileMessage')} - {Localize.translateLocal('iou.error.loseFileMessage')} - - ); - } - - if (message === CONST.COMPANY_CARDS.CONNECTION_ERROR) { - return ( - - {Localize.translateLocal('workspace.companyCards.brokenConnectionErrorFirstPart')} - { - // TODO: re-navigate the user to the bank’s website to re-authenticate https://github.com/Expensify/App/issues/50448 - }} - > - {Localize.translateLocal('workspace.companyCards.brokenConnectionErrorLink')} - - - {Localize.translateLocal('workspace.companyCards.brokenConnectionErrorSecondPart')} + {translateLocal('iou.error.loseFileMessage')} ); } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0b31401d7e25..1e5e5027dc4f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -533,6 +533,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: () => require('../../../../pages/workspace/companyCards/assignCard/AssignCardFeedPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: () => require('../../../../pages/workspace/companyCards/addNew/BankConnection').default, [SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: () => require('../../../../pages/workspace/companyCards/addNew/AddNewCardPage').default, [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardDetailsPage').default, [SCREENS.WORKSPACE.COMPANY_CARD_NAME]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardEditCardNamePage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 2c3b060e0835..7865993d08e9 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -213,6 +213,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.COMPANY_CARDS_NAME, SCREENS.WORKSPACE.COMPANY_CARDS_DETAILS, SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, + SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 579dfe227fb9..9b7061e09ccc 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -620,6 +620,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.route, }, + [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_BANK_CONNECTION.route, + }, [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: { path: ROUTES.WORKSPACE_COMPANY_CARD_DETAILS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 67752a152941..1de3e11f08e4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -822,6 +822,11 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { policyID: string; }; + [SCREENS.WORKSPACE.COMPANY_CARDS_BANK_CONNECTION]: { + policyID: string; + bankName: string; + backTo: Routes; + }; [SCREENS.WORKSPACE.COMPANY_CARD_DETAILS]: { policyID: string; bank: CompanyCardFeed; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 9e5e03acd1b0..7a284ee527b9 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -9,7 +9,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/NetSuiteCustomFieldForm'; import type {OnyxInputOrEntry, Policy, PolicyCategories, PolicyEmployeeList, PolicyTagLists, PolicyTags, Report, TaxRate} from '@src/types/onyx'; -import type {CardFeedData} from '@src/types/onyx/CardFeeds'; import type {ErrorFields, PendingAction, PendingFields} from '@src/types/onyx/OnyxCommon'; import type { ConnectionLastSync, @@ -1146,10 +1145,6 @@ function getWorkflowApprovalsUnavailable(policy: OnyxEntry) { return policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.OPTIONAL || !!policy?.errorFields?.approvalMode; } -function hasPolicyFeedsError(feeds: Record, feedToSkip?: string): boolean { - return Object.entries(feeds).filter(([feedName, feedData]) => feedName !== feedToSkip && !!feedData.errors).length > 0; -} - function getAllPoliciesLength() { return Object.keys(allPolicies ?? {}).length; } @@ -1233,7 +1228,6 @@ export { goBackFromInvalidPolicy, hasAccountingConnections, shouldShowSyncError, - hasPolicyFeedsError, shouldShowCustomUnitsError, shouldShowEmployeeListError, hasIntegrationAutoSync, diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index d4a905d5ef6d..f6e6dbb3729d 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type { @@ -18,10 +18,11 @@ import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, CardFeeds} from '@src/types/onyx'; +import type {Card, CardFeeds, WorkspaceCardsList} from '@src/types/onyx'; import type {AssignCard, AssignCardData} from '@src/types/onyx/AssignCard'; import type {AddNewCardFeedData, AddNewCardFeedStep, CompanyCardFeed} from '@src/types/onyx/CardFeeds'; import type {OnyxData} from '@src/types/onyx/Request'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; type AddNewCompanyCardFlowData = { /** Step to be set in Onyx */ @@ -403,8 +404,6 @@ function unassignWorkspaceCompanyCard(workspaceAccountID: number, bankName: stri function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, bankName: CompanyCardFeed) { const authToken = NetworkStore.getAuthToken(); - const optimisticFeedUpdates = {[bankName]: {errors: null}}; - const failureFeedUpdates = {[bankName]: {errors: {error: CONST.COMPANY_CARDS.CONNECTION_ERROR}}}; const optimisticData: OnyxUpdate[] = [ { @@ -437,13 +436,6 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, }, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, - value: { - settings: {companyCards: optimisticFeedUpdates}, - }, - }, ]; const finallyData: OnyxUpdate[] = [ @@ -504,13 +496,6 @@ function updateWorkspaceCompanyCard(workspaceAccountID: number, cardID: string, }, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, - value: { - settings: {companyCards: failureFeedUpdates}, - }, - }, ]; const parameters = { @@ -740,6 +725,41 @@ function openPolicyCompanyCardsFeed(policyID: string, feed: CompanyCardFeed) { API.read(READ_COMMANDS.OPEN_POLICY_COMPANY_CARDS_FEED, parameters); } +/** + * Takes the list of cards divided by workspaces and feeds and returns the flattened non-Expensify cards related to the provided workspace + * + * @param allCardsList the list where cards split by workspaces and feeds and stored under `card_${workspaceAccountID}_${feedName}` keys + * @param workspaceAccountID the workspace account id we want to get cards for + */ +function flatAllCardsList(allCardsList: OnyxCollection, workspaceAccountID: number): Record | undefined { + if (!allCardsList) { + return; + } + + return Object.entries(allCardsList).reduce((acc, [key, allCards]) => { + if (!key.includes(workspaceAccountID.toString()) || key.includes(CONST.EXPENSIFY_CARD.BANK)) { + return acc; + } + const {cardList, ...feedCards} = allCards ?? {}; + Object.assign(acc, feedCards); + return acc; + }, {}); +} + +/** + * Check if any feed card has a broken connection + * + * @param feedCards the list of the cards, related to one or several feeds + * @param [feedToExclude] the feed to ignore during the check, it's useful for checking broken connection error only in the feeds other than the selected one + */ +function checkIfFeedConnectionIsBroken(feedCards: Record | undefined, feedToExclude?: string): boolean { + if (!feedCards || isEmptyObject(feedCards)) { + return false; + } + + return Object.values(feedCards).some((card) => card.bank !== feedToExclude && card.lastScrapeResult !== 200); +} + export { setWorkspaceCompanyCardFeedName, deleteWorkspaceCompanyCardFeed, @@ -757,4 +777,6 @@ export { clearAddNewCardFlow, setAssignCardStepAndData, clearAssignCardStepAndData, + checkIfFeedConnectionIsBroken, + flatAllCardsList, }; diff --git a/src/libs/actions/getCompanyCardBankConnection/index.tsx b/src/libs/actions/getCompanyCardBankConnection/index.tsx index fb6dd9943972..a136a3783136 100644 --- a/src/libs/actions/getCompanyCardBankConnection/index.tsx +++ b/src/libs/actions/getCompanyCardBankConnection/index.tsx @@ -11,7 +11,7 @@ type CompanyCardBankConnection = { isNewDot: string; }; -export default function getCompanyCardBankConnection(policyID?: string, bankName?: string, scrapeMinDate?: string) { +export default function getCompanyCardBankConnection(policyID?: string, bankName?: string) { const bankConnection = Object.keys(CONST.COMPANY_CARDS.BANKS).find((key) => CONST.COMPANY_CARDS.BANKS[key as keyof typeof CONST.COMPANY_CARDS.BANKS] === bankName); if (!bankName || !bankConnection || !policyID) { @@ -23,7 +23,7 @@ export default function getCompanyCardBankConnection(policyID?: string, bankName isNewDot: 'true', domainName: PolicyUtils.getDomainNameForPolicy(policyID), isCorporate: 'true', - scrapeMinDate: scrapeMinDate ?? '', + scrapeMinDate: '', }; const commandURL = getApiRoot({ shouldSkipWebProxy: true, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 2a9b77551c0f..163fa60cb1fd 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -21,17 +21,29 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {isConnectionInProgress} from '@libs/actions/connections'; -import * as CardUtils from '@libs/CardUtils'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; +import {convertToDisplayString} from '@libs/CurrencyUtils'; import getTopmostRouteName from '@libs/Navigation/getTopmostRouteName'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import { + shouldShowPolicy as checkIfShouldShowPolicy, + getWorkspaceAccountID, + goBackFromInvalidPolicy, + hasPolicyCategoriesError, + isPaidGroupPolicy, + isPendingDeletePolicy, + isPolicyAdmin, + isPolicyFeatureEnabled, + shouldShowEmployeeListError, + shouldShowSyncError, + shouldShowTaxRateError, +} from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar, getIcons, getPolicyExpenseChat, getReportName, getReportOfflinePendingActionAndErrors} from '@libs/ReportUtils'; import type {FullScreenNavigatorParamList} from '@navigation/types'; -import * as App from '@userActions/App'; -import * as Policy from '@userActions/Policy/Policy'; -import * as ReimbursementAccount from '@userActions/ReimbursementAccount'; +import {confirmReadyToOpenApp} from '@userActions/App'; +import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@userActions/CompanyCards'; +import {clearErrors, openPolicyInitialPage, removeWorkspace, updateGeneralSettings} from '@userActions/Policy/Policy'; +import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -76,20 +88,20 @@ type PolicyFeatureStates = Record; function dismissError(policyID: string, pendingAction: PendingAction | undefined) { if (!policyID || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD) { - PolicyUtils.goBackFromInvalidPolicy(); - Policy.removeWorkspace(policyID); + goBackFromInvalidPolicy(); + removeWorkspace(policyID); } else { - Policy.clearErrors(policyID); + clearErrors(policyID); } } function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: WorkspaceInitialPageProps) { const styles = useThemeStyles(); const policy = policyDraft?.id ? policyDraft : policyProp; - const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policy?.id); + const workspaceAccountID = getWorkspaceAccountID(policy?.id); const [isCurrencyModalOpen, setIsCurrencyModalOpen] = useState(false); const hasPolicyCreationError = !!(policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors)); - const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`); const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`); @@ -97,7 +109,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${route.params?.policyID}`); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); const {login, accountID} = useCurrentUserPersonalDetails(); - const hasSyncError = PolicyUtils.shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy)); + const hasSyncError = shouldShowSyncError(policy, isConnectionInProgress(connectionSyncProgress, policy)); const waitForNavigate = useWaitForNavigation(); const {singleExecution, isExecuting} = useSingleExecution(); const activeRoute = useNavigationState(getTopmostRouteName); @@ -131,7 +143,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac if (policyDraft?.id) { return; } - Policy.openPolicyInitialPage(route.params.policyID); + openPolicyInitialPage(route.params.policyID); }, [policyDraft?.id, route.params.policyID]); useNetwork({onReconnect: fetchPolicyData}); @@ -153,20 +165,19 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac /** Call update workspace currency and hide the modal */ const confirmCurrencyChangeAndHideModal = useCallback(() => { - Policy.updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD); + updateGeneralSettings(policyID, policyName, CONST.CURRENCY.USD); setIsCurrencyModalOpen(false); - ReimbursementAccount.navigateToBankAccountRoute(policyID); + navigateToBankAccountRoute(policyID); }, [policyID, policyName]); - const hasMembersError = PolicyUtils.shouldShowEmployeeListError(policy); - const hasPolicyCategoryError = PolicyUtils.hasPolicyCategoriesError(policyCategories); + const hasMembersError = shouldShowEmployeeListError(policy); + const hasPolicyCategoryError = hasPolicyCategoriesError(policyCategories); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.name ?? {}) || !isEmptyObject(policy?.errorFields?.avatarURL ?? {}) || !isEmptyObject(policy?.errorFields?.ouputCurrency ?? {}) || !isEmptyObject(policy?.errorFields?.address ?? {}); - const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy, login); - const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); + const shouldShowProtectedItems = isPolicyAdmin(policy, login); const [featureStates, setFeatureStates] = useState(policyFeatureStates); const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = []; @@ -177,7 +188,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac setFeatureStates((currentFeatureStates) => { const newFeatureStates = {} as PolicyFeatureStates; (Object.keys(policy?.pendingFields ?? {}) as PolicyFeatureName[]).forEach((key) => { - const isFeatureEnabled = PolicyUtils.isPolicyFeatureEnabled(policy, key); + const isFeatureEnabled = isPolicyFeatureEnabled(policy, key); newFeatureStates[key] = prevPendingFields?.[key] !== policy?.pendingFields?.[key] || isOffline || !policy?.pendingFields?.[key] ? isFeatureEnabled : currentFeatureStates[key]; }); @@ -189,7 +200,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac }, [policy, isOffline, policyFeatureStates, prevPendingFields]); useEffect(() => { - App.confirmReadyToOpenApp(); + confirmReadyToOpenApp(); }, []); if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED]) { @@ -212,14 +223,14 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac } if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED]) { - const hasPolicyFeedsError = PolicyUtils.hasPolicyFeedsError(CardUtils.getCompanyFeeds(cardFeeds)); + const hasBrokenFeedConnection = checkIfFeedConnectionIsBroken(flatAllCardsList(allFeedsCards, workspaceAccountID)); protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.companyCards', icon: Expensicons.CreditCard, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.COMPANY_CARDS, - brickRoadIndicator: hasPolicyFeedsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + brickRoadIndicator: hasBrokenFeedConnection ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } @@ -259,7 +270,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac icon: Expensicons.InvoiceGeneric, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_INVOICES.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.INVOICES, - badgeText: CurrencyUtils.convertToDisplayString(policy?.invoice?.bankAccount?.stripeConnectAccountBalance ?? 0, currencyCode), + badgeText: convertToDisplayString(policy?.invoice?.bankAccount?.stripeConnectAccountBalance ?? 0, currencyCode), }); } @@ -288,7 +299,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac icon: Expensicons.Coins, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_TAXES.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.TAXES, - brickRoadIndicator: PolicyUtils.shouldShowTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + brickRoadIndicator: shouldShowTaxRateError(policy) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } @@ -333,24 +344,24 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac brickRoadIndicator: hasMembersError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, routeName: SCREENS.WORKSPACE.MEMBERS, }, - ...(isPaidGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), + ...(isPaidGroupPolicy(policy) && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), ]; const prevPolicy = usePrevious(policy); const prevProtectedMenuItems = usePrevious(protectedCollectPolicyMenuItems); const enabledItem = protectedCollectPolicyMenuItems.find((curItem) => !prevProtectedMenuItems.some((prevItem) => curItem.routeName === prevItem.routeName)); - const shouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); - const prevShouldShowPolicy = useMemo(() => PolicyUtils.shouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); + const shouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(policy, isOffline, currentUserLogin), [policy, isOffline, currentUserLogin]); + const prevShouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(prevPolicy, isOffline, currentUserLogin), [prevPolicy, isOffline, currentUserLogin]); // We check shouldShowPolicy and prevShouldShowPolicy to prevent the NotFound view from showing right after we delete the workspace // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = isEmptyObject(policy) || (!shouldShowPolicy && !prevShouldShowPolicy); useEffect(() => { - if (isEmptyObject(prevPolicy) || PolicyUtils.isPendingDeletePolicy(prevPolicy) || !PolicyUtils.isPendingDeletePolicy(policy)) { + if (isEmptyObject(prevPolicy) || isPendingDeletePolicy(prevPolicy) || !isPendingDeletePolicy(policy)) { return; } - PolicyUtils.goBackFromInvalidPolicy(); + goBackFromInvalidPolicy(); }, [policy, prevPolicy]); // We are checking if the user can access the route. diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx index 29d0dfb0c6af..980d8e08fb77 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage.tsx @@ -10,15 +10,15 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as CardUtils from '@libs/CardUtils'; +import {getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, getSelectedFeed} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; -import * as PolicyUtils from '@libs/PolicyUtils'; +import {getWorkspaceAccountID} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; -import * as Card from '@userActions/Card'; -import * as CompanyCards from '@userActions/CompanyCards'; +import {updateSelectedFeed} from '@userActions/Card'; +import {checkIfFeedConnectionIsBroken, clearAddNewCardFlow} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -34,38 +34,42 @@ type WorkspaceCompanyCardFeedSelectorPageProps = PlatformStackScreenProps ({ - value: feed, - text: CardUtils.getCustomOrFormattedFeedName(feed, cardFeeds?.settings?.companyCardNicknames), - keyForList: feed, - isSelected: feed === selectedFeed, - isDisabled: companyFeeds[feed]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - pendingAction: companyFeeds[feed]?.pendingAction, - brickRoadIndicator: companyFeeds[feed]?.errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, - canShowSeveralIndicators: !!companyFeeds[feed]?.errors, - leftElement: ( - - ), - })); + const feeds: CardFeedListItem[] = (Object.keys(companyFeeds) as CompanyCardFeed[]).map((feed) => { + const isFeedConnectionBroken = checkIfFeedConnectionIsBroken(allFeedsCards?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${feed}`]); + return { + value: feed, + text: getCustomOrFormattedFeedName(feed, cardFeeds?.settings?.companyCardNicknames), + keyForList: feed, + isSelected: feed === selectedFeed, + isDisabled: companyFeeds[feed]?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: companyFeeds[feed]?.pendingAction, + brickRoadIndicator: isFeedConnectionBroken ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, + canShowSeveralIndicators: isFeedConnectionBroken, + leftElement: ( + + ), + }; + }); const goBack = () => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); const selectFeed = (feed: CardFeedListItem) => { - Card.updateSelectedFeed(feed.value, policyID); + updateSelectedFeed(feed.value, policyID); goBack(); }; @@ -95,7 +99,7 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS title={translate('workspace.companyCards.addCards')} icon={Expensicons.Plus} onPress={() => { - CompanyCards.clearAddNewCardFlow(); + clearAddNewCardFlow(); Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_ADD_NEW.getRoute(policyID)); }} /> diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx index 7e4b5fe925f5..559029db3848 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsListHeaderButtons.tsx @@ -5,17 +5,19 @@ import Button from '@components/Button'; import CaretWrapper from '@components/CaretWrapper'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, isCustomFeed} from '@libs/CardUtils'; -import {getWorkspaceAccountID, hasPolicyFeedsError} from '@libs/PolicyUtils'; +import {getBankName, getCardFeedIcon, getCompanyFeeds, getCustomOrFormattedFeedName, isCustomFeed} from '@libs/CardUtils'; +import {getWorkspaceAccountID} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; +import {checkIfFeedConnectionIsBroken, flatAllCardsList} from '@userActions/CompanyCards'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {CompanyCardFeed} from '@src/types/onyx'; @@ -38,21 +40,21 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed, shouldS const styles = useThemeStyles(); const {translate} = useLocalize(); const theme = useTheme(); + const StyleUtils = useStyleUtils(); const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); const workspaceAccountID = getWorkspaceAccountID(policyID); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); + const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`); const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout; const formattedFeedName = getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames); const isCommercialFeed = isCustomFeed(selectedFeed); const companyFeeds = getCompanyFeeds(cardFeeds); const currentFeedData = companyFeeds?.[selectedFeed]; + const bankName = getBankName(selectedFeed); + const isSelectedFeedConnectionBroken = checkIfFeedConnectionIsBroken(allFeedsCards?.[`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`]); return ( - + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))} @@ -70,7 +72,7 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed, shouldS {formattedFeedName} - {hasPolicyFeedsError(companyFeeds, selectedFeed) && ( + {checkIfFeedConnectionIsBroken(flatAllCardsList(allFeedsCards, workspaceAccountID), selectedFeed) && ( - + {isSelectedFeedConnectionBroken && !!bankName && ( + + + + {translate('workspace.companyCards.brokenConnectionErrorFirstPart')} + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_BANK_CONNECTION.getRoute(policyID, bankName, Navigation.getActiveRoute()))} + > + {translate('workspace.companyCards.brokenConnectionErrorLink')} + + {translate('workspace.companyCards.brokenConnectionErrorSecondPart')} + + + )} + ); } diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx index 7dc7a4d370e5..470031f2d3c8 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx @@ -16,7 +16,7 @@ import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import {getWorkspaceAccountID, isDeletedPolicyEmployee} from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; -import {openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAssignCardStepAndData} from '@userActions/CompanyCards'; +import {checkIfFeedConnectionIsBroken, openPolicyCompanyCardsFeed, openPolicyCompanyCardsPage, setAssignCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -55,18 +55,19 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { const isPending = !!selectedFeedData?.pending; const isFeedAdded = !isPending && !isNoFeed; const isFeedExpired = isSelectedFeedExpired(selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined); + const isFeedConnectionBroken = checkIfFeedConnectionIsBroken(cards); const fetchCompanyCards = useCallback(() => { openPolicyCompanyCardsPage(policyID, workspaceAccountID); }, [policyID, workspaceAccountID]); const {isOffline} = useNetwork({onReconnect: fetchCompanyCards}); - const isLoading = !isOffline && (!cardFeeds || (cardFeeds.isLoading && !cardsList)); + const isLoading = !isOffline && (!cardFeeds || (!!cardFeeds.isLoading && !cardsList)); useFocusEffect(fetchCompanyCards); useEffect(() => { - if (!!isLoading || !selectedFeed || isPending) { + if (isLoading || !selectedFeed || isPending) { return; } @@ -149,7 +150,7 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) { cardsList={cardsList} policyID={policyID} handleAssignCard={handleAssignCard} - isDisabledAssignCardButton={!selectedFeedData || !!selectedFeedData?.errors} + isDisabledAssignCardButton={!selectedFeedData || isFeedConnectionBroken} /> )} diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx index 76bf9b2bd967..903abeb0d22f 100644 --- a/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/addNew/BankConnection/index.native.tsx @@ -3,7 +3,6 @@ import {ActivityIndicator} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import type {WebViewNavigation} from 'react-native-webview'; import {WebView} from 'react-native-webview'; -import type {ValueOf} from 'type-fest'; import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -17,17 +16,21 @@ import {checkIfNewFeedConnected} from '@libs/CardUtils'; import getUAForWebView from '@libs/getUAForWebView'; import Navigation from '@libs/Navigation/Navigation'; import {getWorkspaceAccountID} from '@libs/PolicyUtils'; +import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; 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'; type BankConnectionStepProps = { policyID?: string; + route?: PlatformStackRouteProp; }; -function BankConnection({policyID}: BankConnectionStepProps) { +function BankConnection({policyID: policyIDFromProps, route}: BankConnectionStepProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -35,7 +38,9 @@ function BankConnection({policyID}: BankConnectionStepProps) { const [session] = useOnyx(ONYXKEYS.SESSION); const authToken = session?.authToken ?? null; const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const bankName: ValueOf | undefined = addNewCard?.data?.selectedBank; + const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; + const policyID = policyIDFromProps ?? policyIDFromRoute; + const bankName = bankNameFromRoute ?? addNewCard?.data?.selectedBank; const url = getCompanyCardBankConnection(policyID, bankName); const workspaceAccountID = getWorkspaceAccountID(policyID); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); @@ -46,6 +51,10 @@ function BankConnection({policyID}: BankConnectionStepProps) { const renderLoading = () => ; const handleBackButtonPress = () => { + if (backTo) { + Navigation.goBack(backTo); + return; + } if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) { setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK}); return; @@ -85,7 +94,7 @@ function BankConnection({policyID}: BankConnectionStepProps) { shouldEnableMaxHeight > diff --git a/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx b/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx index e563f9020a39..fb7731589e84 100644 --- a/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/addNew/BankConnection/index.tsx @@ -1,6 +1,5 @@ import React, {useCallback, useEffect, useMemo, useState} from 'react'; import {useOnyx} from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import BlockingView from '@components/BlockingViews/BlockingView'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; @@ -13,25 +12,31 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {checkIfNewFeedConnected} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getWorkspaceAccountID} from '@libs/PolicyUtils'; +import type {PlatformStackRouteProp} from '@navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; 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 openBankConnection from './openBankConnection'; let customWindow: Window | null = null; type BankConnectionStepProps = { policyID?: string; + route?: PlatformStackRouteProp; }; -function BankConnection({policyID}: BankConnectionStepProps) { +function BankConnection({policyID: policyIDFromProps, route}: BankConnectionStepProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD); - const bankName: ValueOf | undefined = addNewCard?.data?.selectedBank; + const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {}; + const policyID = policyIDFromProps ?? policyIDFromRoute; + const bankName = bankNameFromRoute ?? addNewCard?.data?.selectedBank; const workspaceAccountID = getWorkspaceAccountID(policyID); const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`); const prevFeedsData = usePrevious(cardFeeds?.settings?.oAuthAccountDetails); @@ -49,6 +54,10 @@ function BankConnection({policyID}: BankConnectionStepProps) { const handleBackButtonPress = () => { customWindow?.close(); + if (backTo) { + Navigation.goBack(backTo); + return; + } if (bankName === CONST.COMPANY_CARDS.BANKS.BREX) { setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_BANK}); return; @@ -88,7 +97,7 @@ function BankConnection({policyID}: BankConnectionStepProps) { return ( ; /** Direct card feed data */ @@ -58,9 +55,6 @@ type DirectCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether any actions are pending */ pending?: boolean; - - /** Broken connection errors */ - errors?: OnyxCommon.Errors; }>; /** Card feed data */