diff --git a/src/components/Switch.tsx b/src/components/Switch.tsx index 1693bafe323d..30616e64755d 100644 --- a/src/components/Switch.tsx +++ b/src/components/Switch.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import {Animated} from 'react-native'; +import {Animated, InteractionManager} from 'react-native'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useNativeDriver from '@libs/useNativeDriver'; @@ -44,8 +44,16 @@ function Switch({isOn, onToggle, accessibilityLabel, disabled}: SwitchProps) { onToggle(!isOn)} - onLongPress={() => onToggle(!isOn)} + onPress={() => { + InteractionManager.runAfterInteractions(() => { + onToggle(!isOn); + }); + }} + onLongPress={() => { + InteractionManager.runAfterInteractions(() => { + onToggle(!isOn); + }); + }} role={CONST.ROLE.SWITCH} aria-checked={isOn} accessibilityLabel={accessibilityLabel} diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index cd375b580d85..0c6c371c0f09 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -3806,7 +3806,9 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) { new Promise((resolve) => { resolve(); }).then(() => { - Navigation.navigate(featureRoute); + requestAnimationFrame(() => { + Navigation.navigate(featureRoute); + }); }); } diff --git a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx index 3bcdc1fe3303..a1337a38795a 100644 --- a/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx @@ -1,9 +1,11 @@ /* eslint-disable rulesdir/no-negated-variables */ -import React, {useEffect} from 'react'; +import {useIsFocused} from '@react-navigation/native'; +import React, {useEffect, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as Policy from '@userActions/Policy'; @@ -35,7 +37,31 @@ type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFou function FeatureEnabledAccessOrNotFoundComponent(props: FeatureEnabledAccessOrNotFoundComponentProps) { const isPolicyIDInRoute = !!props.policyID?.length; const shouldShowFullScreenLoadingIndicator = props.isLoadingReportData !== false && (!Object.entries(props.policy ?? {}).length || !props.policy?.id); - const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); + const isFeatureEnabled = PolicyUtils.isPolicyFeatureEnabled(props.policy, props.featureName); + const [isPolicyFeatureEnabled, setIsPolicyFeatureEnabled] = useState(isFeatureEnabled); + const shouldShowNotFoundPage = isEmptyObject(props.policy) || !props.policy?.id || !isPolicyFeatureEnabled; + const pendingField = props.policy?.pendingFields?.[props.featureName]; + const [isFeatureScreenOpen, setIsFeatureScreenOpen] = useState(false); + const isFocused = useIsFocused(); + const {isOffline} = useNetwork(); + + useEffect(() => { + if (!isFeatureScreenOpen && isFocused) { + setIsFeatureScreenOpen(true); + setIsPolicyFeatureEnabled(isFeatureEnabled); + return; + } + if (!isFocused) { + setIsFeatureScreenOpen(false); + return; + } + setIsPolicyFeatureEnabled((isPrevFeatureEnabled) => { + if (!pendingField || isOffline) { + return isFeatureEnabled; + } + return isPrevFeatureEnabled; + }); + }, [isFocused, pendingField, isOffline, isFeatureEnabled, isFeatureScreenOpen]); useEffect(() => { if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) { diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 2f32034391a5..0eb5a38c3270 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -14,6 +14,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useSingleExecution from '@hooks/useSingleExecution'; @@ -33,6 +34,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import type * as OnyxTypes from '@src/types/onyx'; +import type {PolicyFeatureName} from '@src/types/onyx/Policy'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; @@ -86,6 +88,20 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const activeRoute = useNavigationState(getTopmostWorkspacesCentralPaneName); const {translate} = useLocalize(); const {canUseAccountingIntegrations} = usePermissions(); + const {isOffline} = useNetwork(); + + const prevPendingFields = usePrevious(policy?.pendingFields); + const policyFeatureStates = useMemo( + () => ({ + [CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED]: policy?.areDistanceRatesEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED]: policy?.areWorkflowsEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED]: policy?.areCategoriesEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]: policy?.areTagsEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]: policy?.tax?.trackingEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED]: policy?.areConnectionsEnabled, + }), + [policy], + ) as Record; const policyID = policy?.id ?? ''; const policyName = policy?.name ?? ''; @@ -122,6 +138,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy); const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); const isFreeGroupPolicy = PolicyUtils.isFreeGroupPolicy(policy); + const [featureStates, setFeatureStates] = useState(policyFeatureStates); const protectedFreePolicyMenuItems: WorkspaceMenuItem[] = [ { @@ -167,7 +184,28 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = []; - if (policy?.areDistanceRatesEnabled) { + useEffect(() => { + setFeatureStates((currentFeatureStates) => { + const newFeatureStates = {} as Record; + const keys = Object.keys(policy?.pendingFields ?? {}) as PolicyFeatureName[]; + keys.forEach((key) => { + const isFeatureEnabled = PolicyUtils.isPolicyFeatureEnabled(policy, key); + if (prevPendingFields?.[key] !== policy?.pendingFields?.[key] || isOffline || !policy?.pendingFields?.[key]) { + newFeatureStates[key] = isFeatureEnabled; + + return; + } + + newFeatureStates[key] = currentFeatureStates[key]; + }); + return { + ...policyFeatureStates, + ...newFeatureStates, + }; + }); + }, [policy, isOffline, policyFeatureStates, prevPendingFields]); + + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_DISTANCE_RATES_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.distanceRates', icon: Expensicons.Car, @@ -176,7 +214,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } - if (policy?.areWorkflowsEnabled) { + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.workflows', icon: Expensicons.Workflows, @@ -186,7 +224,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } - if (policy?.areCategoriesEnabled) { + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.categories', icon: Expensicons.Folder, @@ -196,7 +234,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } - if (policy?.areTagsEnabled) { + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_TAGS_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.tags', icon: Expensicons.Tag, @@ -205,7 +243,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } - if (policy?.tax?.trackingEnabled) { + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.taxes', icon: Expensicons.Tax, @@ -215,7 +253,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, reimbursementAcc }); } - if (policy?.areConnectionsEnabled && canUseAccountingIntegrations) { + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED] && canUseAccountingIntegrations) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.accounting', icon: Expensicons.Sync,