diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index e1594a56eed4..0d999b1e6951 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1815,7 +1815,11 @@ function updateAddress(policyID: string, newAddress: CompanyAddress) { /** * Removes an error after trying to delete a workspace */ -function clearDeleteWorkspaceError(policyID: string) { +function clearDeleteWorkspaceError(policyID: string | undefined) { + if (!policyID) { + return; + } + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { pendingAction: null, errors: null, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 2c335df1a787..520e1cd9f34a 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -1,4 +1,4 @@ -import {findFocusedRoute, useFocusEffect, useNavigationState} from '@react-navigation/native'; +import {findFocusedRoute, useFocusEffect, useIsFocused, useNavigationState} from '@react-navigation/native'; import {emailSelector} from '@selectors/Session'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -114,6 +114,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const policy = policyDraft?.id ? policyDraft : policyProp; const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID; const hasPolicyCreationError = policy?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && !isEmptyObject(policy.errors); + const isFocused = useIsFocused(); const [cardFeeds] = useCardFeeds(policy?.id); const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: true}); const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policy?.id}`, {canBeMissing: true}); @@ -413,7 +414,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const prevPolicy = usePrevious(policy); - const shouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(policy, false, currentUserLogin), [policy, currentUserLogin]); + const shouldShowPolicy = useMemo(() => checkIfShouldShowPolicy(policy, true, currentUserLogin), [policy, currentUserLogin]); const isPendingDelete = isPendingDeletePolicy(policy); const prevIsPendingDelete = isPendingDeletePolicy(prevPolicy); // We check isPendingDelete and prevIsPendingDelete to prevent the NotFound view from showing right after we delete the workspace @@ -421,11 +422,11 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac const shouldShowNotFoundPage = !shouldShowPolicy && (!isPendingDelete || prevIsPendingDelete); useEffect(() => { - if (isEmptyObject(prevPolicy) || prevIsPendingDelete || !isPendingDelete) { + if (!isFocused || isEmptyObject(prevPolicy) || prevIsPendingDelete || !isPendingDelete) { return; } goBackFromInvalidPolicy(); - }, [isPendingDelete, policy, prevIsPendingDelete, prevPolicy]); + }, [isFocused, isPendingDelete, policy, prevIsPendingDelete, prevPolicy]); // We are checking if the user can access the route. // If user can't access the route, we are dismissing any modals that are open when the NotFound view is shown diff --git a/src/pages/workspace/WorkspaceOverviewPage.tsx b/src/pages/workspace/WorkspaceOverviewPage.tsx index 9363bf866006..ff71b47af43f 100644 --- a/src/pages/workspace/WorkspaceOverviewPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewPage.tsx @@ -1,4 +1,4 @@ -import {useFocusEffect} from '@react-navigation/native'; +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; import {accountIDSelector} from '@selectors/Session'; import React, {useCallback, useContext, useEffect, useRef, useState} from 'react'; import type {ImageStyle, StyleProp} from 'react-native'; @@ -22,6 +22,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePayAndDowngrade from '@hooks/usePayAndDowngrade'; import usePermissions from '@hooks/usePermissions'; +import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -30,6 +31,7 @@ import {clearInviteDraft, clearWorkspaceOwnerChangeFlow, requestWorkspaceOwnerCh import { calculateBillNewDot, clearAvatarErrors, + clearDeleteWorkspaceError, clearPolicyErrorField, deleteWorkspace, deleteWorkspaceAvatar, @@ -38,11 +40,11 @@ import { updateWorkspaceAvatar, } from '@libs/actions/Policy/Policy'; import {filterInactiveCards} from '@libs/CardUtils'; -import {getLatestErrorField} from '@libs/ErrorUtils'; +import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types'; -import {getUserFriendlyWorkspaceType, goBackFromInvalidPolicy, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyOwner} from '@libs/PolicyUtils'; +import {getUserFriendlyWorkspaceType, goBackFromInvalidPolicy, isPendingDeletePolicy, isPolicyAdmin as isPolicyAdminPolicyUtils, isPolicyOwner} from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; import StringUtils from '@libs/StringUtils'; @@ -160,6 +162,13 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const [lastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {canBeMissing: true}); const {isBetaEnabled} = usePermissions(); + const isFocused = useIsFocused(); + const isPendingDelete = isPendingDeletePolicy(policy); + const prevIsPendingDelete = usePrevious(isPendingDelete); + const [isDeleteWorkspaceErrorModalOpen, setIsDeleteWorkspaceErrorModalOpen] = useState(false); + const policyLastErrorMessage = getLatestErrorMessage(policy); + const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); + const fetchPolicyData = useCallback(() => { if (policyDraft?.id) { return; @@ -167,7 +176,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa openPolicyProfilePage(route.params.policyID); }, [policyDraft?.id, route.params.policyID]); - useNetwork({onReconnect: fetchPolicyData}); + const {isOffline} = useNetwork({onReconnect: fetchPolicyData}); // We have the same focus effect in the WorkspaceInitialPage, this way we can get the policy data in narrow // as well as in the wide layout when looking at policy settings. @@ -200,16 +209,19 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa const dropdownMenuRef = useRef<{setIsMenuVisible: (visible: boolean) => void} | null>(null); - const confirmDeleteAndHideModal = useCallback(() => { + const confirmDelete = useCallback(() => { if (!policy?.id || !policyName) { return; } deleteWorkspace(policy.id, policyName, lastAccessedWorkspacePolicyID, defaultCardFeeds, reportsToArchive, transactionViolations, reimbursementAccountError, lastPaymentMethod); - setIsDeleteModalOpen(false); - goBackFromInvalidPolicy(); }, [policy?.id, policyName, lastAccessedWorkspacePolicyID, defaultCardFeeds, reportsToArchive, transactionViolations, reimbursementAccountError, lastPaymentMethod]); + const hideDeleteWorkspaceErrorModal = () => { + setIsDeleteWorkspaceErrorModalOpen(false); + clearDeleteWorkspaceError(policy?.id); + }; + useEffect(() => { if (isLoadingBill) { return; @@ -217,6 +229,18 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa dropdownMenuRef.current?.setIsMenuVisible(false); }, [isLoadingBill]); + useEffect(() => { + if (!isFocused || !prevIsPendingDelete || isPendingDelete) { + return; + } + setIsDeleteModalOpen(false); + if (!policyLastErrorMessage) { + goBackFromInvalidPolicy(); + return; + } + setIsDeleteWorkspaceErrorModalOpen(true); + }, [isFocused, isPendingDelete, prevIsPendingDelete, policyLastErrorMessage]); + const onDeleteWorkspace = useCallback(() => { if (shouldCalculateBillNewDot()) { setIsDeletingPaidWorkspace(true); @@ -224,8 +248,13 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa return; } + if (isOffline) { + setShouldShowOfflineModal(true); + return; + } + setIsDeleteModalOpen(true); - }, [setIsDeletingPaidWorkspace]); + }, [isOffline, setIsDeletingPaidWorkspace]); const handleBackButtonPress = () => { if (isComingFromGlobalReimbursementsFlow) { @@ -515,13 +544,33 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa setIsDeleteModalOpen(false)} prompt={hasCardFeedOrExpensifyCard ? translate('workspace.common.deleteWithCardsConfirmation') : translate('workspace.common.deleteConfirmation')} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} + isConfirmLoading={isPendingDeletePolicy(policy)} danger /> + + setShouldShowOfflineModal(false)} + onCancel={() => setShouldShowOfflineModal(false)} + confirmText={translate('common.buttonConfirm')} + prompt={translate('common.offlinePrompt')} + shouldShowCancelButton={false} + /> )} diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index 8b9ec811b53c..fe1ff602421f 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -30,6 +30,7 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePayAndDowngrade from '@hooks/usePayAndDowngrade'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchResults from '@hooks/useSearchResults'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -41,12 +42,21 @@ import {clearWorkspaceOwnerChangeFlow, requestWorkspaceOwnerChange} from '@libs/ import {calculateBillNewDot, clearDeleteWorkspaceError, clearDuplicateWorkspace, clearErrors, deleteWorkspace, leaveWorkspace, removeWorkspace} from '@libs/actions/Policy/Policy'; import {callFunctionIfActionIsAllowed} from '@libs/actions/Session'; import {filterInactiveCards} from '@libs/CardUtils'; +import {getLatestErrorMessage} from '@libs/ErrorUtils'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; import type {AuthScreensParamList} from '@libs/Navigation/types'; -import {getDefaultApprover, getPolicy, getPolicyBrickRoadIndicatorStatus, getUberConnectionErrorDirectlyFromPolicy, isPolicyAdmin, shouldShowPolicy} from '@libs/PolicyUtils'; +import { + getDefaultApprover, + getPolicy, + getPolicyBrickRoadIndicatorStatus, + getUberConnectionErrorDirectlyFromPolicy, + isPendingDeletePolicy, + isPolicyAdmin, + shouldShowPolicy, +} from '@libs/PolicyUtils'; import {getDefaultWorkspaceAvatar} from '@libs/ReportUtils'; import shouldRenderTransferOwnerButton from '@libs/shouldRenderTransferOwnerButton'; import {shouldCalculateBillNewDot as shouldCalculateBillNewDotFn} from '@libs/SubscriptionUtils'; @@ -125,9 +135,12 @@ function WorkspacesListPage() { const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [policyIDToDelete, setPolicyIDToDelete] = useState(); + // The workspace was deleted in this page const [policyNameToDelete, setPolicyNameToDelete] = useState(); const {reportsToArchive, transactionViolations} = useTransactionViolationOfWorkspace(policyIDToDelete); const {setIsDeletingPaidWorkspace, isLoadingBill}: {setIsDeletingPaidWorkspace: (value: boolean) => void; isLoadingBill: boolean | undefined} = usePayAndDowngrade(setIsDeleteModalOpen); + const [isDeleteWorkspaceErrorModalOpen, setIsDeleteWorkspaceErrorModalOpen] = useState(false); + const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false); const [loadingSpinnerIconIndex, setLoadingSpinnerIconIndex] = useState(null); @@ -148,13 +161,17 @@ function WorkspacesListPage() { // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 // eslint-disable-next-line deprecation/deprecation const policyToDelete = getPolicy(policyIDToDelete); + const prevPolicyToDelete = usePrevious(policyToDelete); const hasCardFeedOrExpensifyCard = !isEmptyObject(cardFeeds) || !isEmptyObject(cardsList) || // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ((policyToDelete?.areExpensifyCardsEnabled || policyToDelete?.areCompanyCardsEnabled) && policyToDelete?.workspaceAccountID); + const policyToDeleteLatestErrorMessage = getLatestErrorMessage(policyToDelete); + const isPendingDelete = isPendingDeletePolicy(policyToDelete); + const prevIsPendingDelete = isPendingDeletePolicy(prevPolicyToDelete); - const confirmDeleteAndHideModal = () => { + const confirmDelete = () => { if (!policyIDToDelete || !policyNameToDelete) { return; } @@ -169,7 +186,15 @@ function WorkspacesListPage() { reimbursementAccountError, lastPaymentMethod, ); - setIsDeleteModalOpen(false); + }; + + const hideDeleteWorkspaceErrorModal = () => { + setIsDeleteWorkspaceErrorModalOpen(false); + setPolicyIDToDelete(undefined); + if (!policyToDelete) { + return; + } + dismissWorkspaceError(policyToDelete.id, policyToDelete.pendingAction); }; const shouldCalculateBillNewDot: boolean = shouldCalculateBillNewDotFn(); @@ -198,6 +223,17 @@ function WorkspacesListPage() { [session?.accountID], ); + useEffect(() => { + if (!prevIsPendingDelete || isPendingDelete) { + return; + } + setIsDeleteModalOpen(false); + if (!isFocused || !policyToDeleteLatestErrorMessage) { + return; + } + setIsDeleteWorkspaceErrorModalOpen(true); + }, [isPendingDelete, prevIsPendingDelete, isFocused, policyToDeleteLatestErrorMessage]); + /** * Gets the menu item for each workspace */ @@ -265,6 +301,13 @@ function WorkspacesListPage() { return; } + if (isOffline) { + setPolicyIDToDelete(undefined); + setPolicyNameToDelete(undefined); + setShouldShowOfflineModal(true); + return; + } + setIsDeleteModalOpen(true); }, shouldKeepModalOpen: shouldCalculateBillNewDot, @@ -289,6 +332,7 @@ function WorkspacesListPage() { errors={item.errors} style={styles.mb2} shouldShowErrorMessages={false} + shouldHideOnDelete={false} > shouldShowPolicy(policy, isOffline, session?.email)) + .filter((policy): policy is PolicyType => shouldShowPolicy(policy, true, session?.email)) .map((policy): WorkspaceItem => { const receiptUberBrickRoadIndicator = getUberConnectionErrorDirectlyFromPolicy(policy as OnyxEntry) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined; @@ -418,7 +463,7 @@ function WorkspacesListPage() { employeeList: policy.employeeList, }; }); - }, [reimbursementAccount?.errors, policies, isOffline, session?.email, allConnectionSyncProgresses, theme.textLight, navigateToWorkspace]); + }, [reimbursementAccount?.errors, policies, session?.email, allConnectionSyncProgresses, theme.textLight, navigateToWorkspace]); const filterWorkspace = useCallback((workspace: WorkspaceItem, inputValue: string) => workspace.title.toLowerCase().includes(inputValue), []); const sortWorkspace = useCallback((workspaceItems: WorkspaceItem[]) => workspaceItems.sort((a, b) => localeCompare(a.title, b.title)), [localeCompare]); @@ -570,13 +615,33 @@ function WorkspacesListPage() { setIsDeleteModalOpen(false)} prompt={hasCardFeedOrExpensifyCard ? translate('workspace.common.deleteWithCardsConfirmation') : translate('workspace.common.deleteConfirmation')} confirmText={translate('common.delete')} cancelText={translate('common.cancel')} + isConfirmLoading={isPendingDelete} danger /> + + setShouldShowOfflineModal(false)} + onCancel={() => setShouldShowOfflineModal(false)} + confirmText={translate('common.buttonConfirm')} + prompt={translate('common.offlinePrompt')} + shouldShowCancelButton={false} + /> {shouldDisplayLHB && } );