Skip to content

Commit

Permalink
Merge pull request #37902 from rezkiy37/feature/37785-restrict-access…
Browse files Browse the repository at this point in the history
…-WorkspaceMoreFeaturesPage

Ensure that disabled feature page cannot be accessed using deep-link
  • Loading branch information
mountiny authored Mar 20, 2024
2 parents 30b809c + c13cda3 commit b527962
Show file tree
Hide file tree
Showing 28 changed files with 992 additions and 724 deletions.
9 changes: 9 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,15 @@ const CONST = {
MAKE_MEMBER: 'makeMember',
MAKE_ADMIN: 'makeAdmin',
},
MORE_FEATURES: {
ARE_CATEGORIES_ENABLED: 'areCategoriesEnabled',
ARE_TAGS_ENABLED: 'areTagsEnabled',
ARE_DISTANCE_RATES_ENABLED: 'areDistanceRatesEnabled',
ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled',
ARE_REPORTFIELDS_ENABLED: 'areReportFieldsEnabled',
ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled',
ARE_TAXES_ENABLED: 'tax',
},
CATEGORIES_BULK_ACTION_TYPES: {
DELETE: 'delete',
DISABLE: 'disable',
Expand Down
10 changes: 10 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {PersonalDetailsList, Policy, PolicyCategories, PolicyMembers, PolicyTagList, PolicyTags} from '@src/types/onyx';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import Navigation from './Navigation/Navigation';
Expand Down Expand Up @@ -276,6 +277,14 @@ function goBackFromInvalidPolicy() {
Navigation.navigate(ROUTES.SETTINGS_WORKSPACES);
}

function isPolicyFeatureEnabled(policy: OnyxEntry<Policy> | EmptyObject, featureName: PolicyFeatureName): boolean {
if (featureName === CONST.POLICY.MORE_FEATURES.ARE_TAXES_ENABLED) {
return Boolean(policy?.tax?.trackingEnabled);
}

return Boolean(policy?.[featureName]);
}

export {
getActivePolicies,
hasAccountingConnections,
Expand Down Expand Up @@ -306,6 +315,7 @@ export {
getPathWithoutPolicyID,
getPolicyMembersByIdWithoutCurrentUser,
goBackFromInvalidPolicy,
isPolicyFeatureEnabled,
hasTaxRateError,
hasPolicyCategoriesError,
};
Expand Down
10 changes: 9 additions & 1 deletion src/libs/actions/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3403,7 +3403,15 @@ function navigateWhenEnableFeature(policyID: string, featureRoute: Route) {
return;
}

Navigation.navigate(featureRoute);
/**
* The app needs to set a navigation action to the microtask queue, it guarantees to execute Onyx.update first, then the navigation action.
* More details - https://github.com/Expensify/App/issues/37785#issuecomment-1989056726.
*/
new Promise<void>((resolve) => {
resolve();
}).then(() => {
Navigation.navigate(featureRoute);
});
}

function enablePolicyCategories(policyID: string, enabled: boolean) {
Expand Down
74 changes: 74 additions & 0 deletions src/pages/workspace/FeatureEnabledAccessOrNotFoundWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/* eslint-disable rulesdir/no-negated-variables */
import React, {useEffect} 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 Navigation from '@libs/Navigation/Navigation';
import * as PolicyUtils from '@libs/PolicyUtils';
import * as Policy from '@userActions/Policy';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type * as OnyxTypes from '@src/types/onyx';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';

type FeatureEnabledAccessOrNotFoundOnyxProps = {
/** The report currently being looked at */
policy: OnyxEntry<OnyxTypes.Policy>;

/** Indicated whether the report data is loading */
isLoadingReportData: OnyxEntry<boolean>;
};

type FeatureEnabledAccessOrNotFoundComponentProps = FeatureEnabledAccessOrNotFoundOnyxProps & {
/** The children to render */
children: ((props: FeatureEnabledAccessOrNotFoundOnyxProps) => React.ReactNode) | React.ReactNode;

/** The report currently being looked at */
policyID: string;

/** The current feature name that the user tries to get access */
featureName: PolicyFeatureName;
};

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);

useEffect(() => {
if (!isPolicyIDInRoute || !isEmptyObject(props.policy)) {
// If the workspace is not required or is already loaded, we don't need to call the API
return;
}

Policy.openWorkspace(props.policyID, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isPolicyIDInRoute, props.policyID]);

if (shouldShowFullScreenLoadingIndicator) {
return <FullscreenLoadingIndicator />;
}

if (shouldShowNotFoundPage) {
return (
<FullPageNotFoundView
shouldShow={shouldShowNotFoundPage}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
shouldForceFullScreen
/>
);
}

return typeof props.children === 'function' ? props.children(props) : props.children;
}

export default withOnyx<FeatureEnabledAccessOrNotFoundComponentProps, FeatureEnabledAccessOrNotFoundOnyxProps>({
policy: {
key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID ?? ''}`,
},
isLoadingReportData: {
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
},
})(FeatureEnabledAccessOrNotFoundComponent);
97 changes: 52 additions & 45 deletions src/pages/workspace/categories/CategorySettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
Expand Down Expand Up @@ -71,53 +73,58 @@ function CategorySettingsPage({route, policyCategories}: CategorySettingsPagePro
return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CategorySettingsPage.displayName}
<FeatureEnabledAccessOrNotFoundWrapper
policyID={route.params.policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED}
>
<HeaderWithBackButton
shouldShowThreeDotsButton
title={route.params.categoryName}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
threeDotsMenuItems={threeDotsMenuItems}
/>
<ConfirmModal
isVisible={deleteCategoryConfirmModalVisible}
onConfirm={deleteCategory}
onCancel={() => setDeleteCategoryConfirmModalVisible(false)}
title={translate('workspace.categories.deleteCategory')}
prompt={translate('workspace.categories.deleteCategoryPrompt')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
<View style={styles.flexGrow1}>
<OfflineWithFeedback
errors={ErrorUtils.getLatestErrorMessageField(policyCategory)}
pendingAction={policyCategory?.pendingFields?.enabled}
errorRowStyles={styles.mh5}
onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)}
>
<View style={[styles.mt2, styles.mh5]}>
<View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}>
<Text>{translate('workspace.categories.enableCategory')}</Text>
<Switch
isOn={policyCategory.enabled}
accessibilityLabel={translate('workspace.categories.enableCategory')}
onToggle={updateWorkspaceRequiresCategory}
/>
</View>
</View>
</OfflineWithFeedback>
<MenuItemWithTopDescription
title={policyCategory.name}
description={translate(`workspace.categories.categoryName`)}
onPress={navigateToEditCategory}
shouldShowRightIcon
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CategorySettingsPage.displayName}
>
<HeaderWithBackButton
shouldShowThreeDotsButton
title={route.params.categoryName}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
threeDotsMenuItems={threeDotsMenuItems}
/>
<ConfirmModal
isVisible={deleteCategoryConfirmModalVisible}
onConfirm={deleteCategory}
onCancel={() => setDeleteCategoryConfirmModalVisible(false)}
title={translate('workspace.categories.deleteCategory')}
prompt={translate('workspace.categories.deleteCategoryPrompt')}
confirmText={translate('common.delete')}
cancelText={translate('common.cancel')}
danger
/>
</View>
</ScreenWrapper>
<View style={styles.flexGrow1}>
<OfflineWithFeedback
errors={ErrorUtils.getLatestErrorMessageField(policyCategory)}
pendingAction={policyCategory?.pendingFields?.enabled}
errorRowStyles={styles.mh5}
onClose={() => Policy.clearCategoryErrors(route.params.policyID, route.params.categoryName)}
>
<View style={[styles.mt2, styles.mh5]}>
<View style={[styles.flexRow, styles.mb5, styles.mr2, styles.alignItemsCenter, styles.justifyContentBetween]}>
<Text>{translate('workspace.categories.enableCategory')}</Text>
<Switch
isOn={policyCategory.enabled}
accessibilityLabel={translate('workspace.categories.enableCategory')}
onToggle={updateWorkspaceRequiresCategory}
/>
</View>
</View>
</OfflineWithFeedback>
<MenuItemWithTopDescription
title={policyCategory.name}
description={translate(`workspace.categories.categoryName`)}
onPress={navigateToEditCategory}
shouldShowRightIcon
/>
</View>
</ScreenWrapper>
</FeatureEnabledAccessOrNotFoundWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
Expand Down
35 changes: 21 additions & 14 deletions src/pages/workspace/categories/CreateCategoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type {PolicyCategories} from '@src/types/onyx';
Expand All @@ -38,21 +40,26 @@ function CreateCategoryPage({route, policyCategories}: CreateCategoryPageProps)
return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateCategoryPage.displayName}
shouldEnableMaxHeight
<FeatureEnabledAccessOrNotFoundWrapper
policyID={route.params.policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED}
>
<HeaderWithBackButton
title={translate('workspace.categories.addCategory')}
onBackButtonPress={Navigation.goBack}
/>
<CategoryForm
onSubmit={createCategory}
policyCategories={policyCategories}
/>
</ScreenWrapper>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={CreateCategoryPage.displayName}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('workspace.categories.addCategory')}
onBackButtonPress={Navigation.goBack}
/>
<CategoryForm
onSubmit={createCategory}
policyCategories={policyCategories}
/>
</ScreenWrapper>
</FeatureEnabledAccessOrNotFoundWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
Expand Down
37 changes: 22 additions & 15 deletions src/pages/workspace/categories/EditCategoryPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper';
import FeatureEnabledAccessOrNotFoundWrapper from '@pages/workspace/FeatureEnabledAccessOrNotFoundWrapper';
import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
Expand Down Expand Up @@ -40,22 +42,27 @@ function EditCategoryPage({route, policyCategories}: EditCategoryPageProps) {
return (
<AdminPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<PaidPolicyAccessOrNotFoundWrapper policyID={route.params.policyID}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={EditCategoryPage.displayName}
shouldEnableMaxHeight
<FeatureEnabledAccessOrNotFoundWrapper
policyID={route.params.policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_CATEGORIES_ENABLED}
>
<HeaderWithBackButton
title={translate('workspace.categories.editCategory')}
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))}
/>
<CategoryForm
onSubmit={editCategory}
categoryName={currentCategoryName}
policyCategories={policyCategories}
/>
</ScreenWrapper>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
style={[styles.defaultModalContainer]}
testID={EditCategoryPage.displayName}
shouldEnableMaxHeight
>
<HeaderWithBackButton
title={translate('workspace.categories.editCategory')}
onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))}
/>
<CategoryForm
onSubmit={editCategory}
categoryName={currentCategoryName}
policyCategories={policyCategories}
/>
</ScreenWrapper>
</FeatureEnabledAccessOrNotFoundWrapper>
</PaidPolicyAccessOrNotFoundWrapper>
</AdminPolicyAccessOrNotFoundWrapper>
);
Expand Down
Loading

0 comments on commit b527962

Please sign in to comment.