Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1815f4f
feat: Update settings pages to conditionally hide Wallet and Preferen…
NicolasBonet May 12, 2026
4d29bc2
feat: Add AGENT access restriction to DelegateNoAccessWrapper across …
NicolasBonet May 12, 2026
89e6861
feat: Integrate DelegateNoAccessWrapper for AGENT access restriction …
NicolasBonet May 12, 2026
834d045
refactor: Clean up JSX structure in MergeResultPage for improved read…
NicolasBonet May 12, 2026
492b82b
feat: Integrate DelegateNoAccessWrapper for AGENT access restriction …
NicolasBonet May 12, 2026
88d4d32
feat: Implement DelegateNoAccessWrapper for AGENT access restriction …
NicolasBonet May 14, 2026
71373f9
refactor: Replace FullScreenLoadingIndicator with ActivityIndicator i…
NicolasBonet May 15, 2026
13fae28
feat: Update InitialSettingsPage to conditionally display Security op…
NicolasBonet May 15, 2026
c2c3e06
feat: Integrate DelegateNoAccessWrapper for AGENT access restriction …
NicolasBonet May 15, 2026
c53b264
feat: Implement withAgentAccessDenied HOC for AGENT access restrictio…
NicolasBonet May 15, 2026
7db229e
refactor: Remove DelegateNoAccessWrapper from multiple settings pages…
NicolasBonet May 15, 2026
d50ca23
refactor: Replace DelegateNoAccessWrapper with withAgentAccessDenied …
NicolasBonet May 15, 2026
935bfe6
refactor: Remove unused imports from LanguagePage, DeviceManagementPa…
NicolasBonet May 15, 2026
793d7ea
test: Add unit tests for withAgentAccessDenied HOC to verify access c…
NicolasBonet May 15, 2026
df78fa4
refactor: Remove duplicate import of withAgentAccessDenied in ModalSt…
NicolasBonet May 15, 2026
3b24a4d
refactor: Apply withAgentAccessDenied HOC to additional settings scre…
NicolasBonet May 18, 2026
9c45429
refactor: Apply withAgentAccessDenied HOC to additional profile and s…
NicolasBonet May 18, 2026
5729d79
refactor: Update import statement for Text component in withAgentAcce…
NicolasBonet May 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6421,6 +6421,7 @@ const CONST = {
DENIED_ACCESS_VARIANTS: {
DELEGATE: 'delegate',
SUBMITTER: 'submitter',
AGENT: 'agent',
},
},
DELEGATE_ROLE_HELP_DOT_ARTICLE_LINK: 'https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/',
Expand Down
19 changes: 14 additions & 5 deletions src/components/DelegateNoAccessWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,26 @@ import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import AccountUtils from '@libs/AccountUtils';
import Navigation from '@libs/Navigation/Navigation';
import {isAgentEmail} from '@libs/SessionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Account} from '@src/types/onyx';
import type {Account, Session} from '@src/types/onyx';
import callOrReturn from '@src/types/utils/callOrReturn';
import FullPageNotFoundView from './BlockingViews/FullPageNotFoundView';

type AccessContext = {
account: OnyxEntry<Account>;
session: OnyxEntry<Session>;
};

const DENIED_ACCESS_VARIANTS = {
// To Restrict All Delegates From Accessing The Page.
[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]: (account: OnyxEntry<Account>) => isDelegate(account),
[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.DELEGATE]: ({account}: AccessContext) => isDelegate(account),
// To Restrict Only Limited Access Delegates From Accessing The Page.
[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.SUBMITTER]: (account: OnyxEntry<Account>) => isSubmitter(account),
} as const satisfies Record<string, (account: OnyxEntry<Account>) => boolean>;
[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.SUBMITTER]: ({account}: AccessContext) => isSubmitter(account),
// To Restrict Agent Accounts From Accessing The Page.
[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.AGENT]: ({session}: AccessContext) => isAgentEmail(session?.email),
} as const satisfies Record<string, (context: AccessContext) => boolean>;

type AccessDeniedVariants = keyof typeof DENIED_ACCESS_VARIANTS;

Expand All @@ -38,9 +46,10 @@ function isSubmitter(account: OnyxEntry<Account>) {

function DelegateNoAccessWrapper({accessDeniedVariants = [], shouldForceFullScreen, onBackButtonPress, ...props}: DelegateNoAccessWrapperProps) {
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [session] = useOnyx(ONYXKEYS.SESSION);
const isPageAccessDenied = accessDeniedVariants.reduce((acc, variant) => {
const accessDeniedFunction = DENIED_ACCESS_VARIANTS[variant];
return acc || accessDeniedFunction(account);
return acc || accessDeniedFunction({account, session});
}, false);
const {shouldUseNarrowLayout} = useResponsiveLayout();

Expand Down
298 changes: 182 additions & 116 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {View} from 'react-native';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
import withAgentAccessDenied from '@libs/Navigation/AppNavigator/withAgentAccessDenied';
import type {SettingsSplitNavigatorParamList} from '@libs/Navigation/types';
import SCREENS from '@src/SCREENS';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
Expand All @@ -13,18 +14,18 @@ const loadInitialSettingsPage = () => require<ReactComponentModule>('../../../..
type Screens = Partial<Record<keyof SettingsSplitNavigatorParamList, () => React.ComponentType>>;

const CENTRAL_PANE_SETTINGS_SCREENS = {
[SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Preferences/PreferencesPage').default,
[SCREENS.SETTINGS.SECURITY]: () => require<ReactComponentModule>('../../../../pages/settings/Security/SecuritySettingsPage').default,
[SCREENS.SETTINGS.PREFERENCES.ROOT]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/settings/Preferences/PreferencesPage').default),
[SCREENS.SETTINGS.SECURITY]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/settings/Security/SecuritySettingsPage').default),
[SCREENS.SETTINGS.COPILOT]: () => require<ReactComponentModule>('../../../../pages/settings/Copilot/CopilotPage').default,
[SCREENS.SETTINGS.PROFILE.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Profile/ProfilePage').default,
[SCREENS.SETTINGS.WALLET.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Wallet/WalletPage').default,
[SCREENS.SETTINGS.AGENTS.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Agents/AgentsPage').default,
[SCREENS.SETTINGS.WALLET.ROOT]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/settings/Wallet/WalletPage').default),
[SCREENS.SETTINGS.AGENTS.ROOT]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/settings/Agents/AgentsPage').default),
[SCREENS.SETTINGS.RULES.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Rules/ExpenseRulesPage').default,
[SCREENS.SETTINGS.HELP]: () => require<ReactComponentModule>('../../../../pages/settings/HelpPage/HelpPage').default,
[SCREENS.SETTINGS.ABOUT]: () => require<ReactComponentModule>('../../../../pages/settings/AboutPage/AboutPage').default,
[SCREENS.SETTINGS.TROUBLESHOOT]: () => require<ReactComponentModule>('../../../../pages/settings/Troubleshoot/TroubleshootPage').default,
[SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require<ReactComponentModule>('../../../../pages/TeachersUnite/SaveTheWorldPage').default,
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: () => require<ReactComponentModule>('../../../../pages/settings/Subscription/SubscriptionSettingsPage').default,
[SCREENS.SETTINGS.SUBSCRIPTION.ROOT]: withAgentAccessDenied(() => require<ReactComponentModule>('../../../../pages/settings/Subscription/SubscriptionSettingsPage').default),
} satisfies Screens;

const Split = createSplitNavigator<SettingsSplitNavigatorParamList>();
Expand Down
20 changes: 20 additions & 0 deletions src/libs/Navigation/AppNavigator/withAgentAccessDenied.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import React from 'react';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import CONST from '@src/CONST';

function withAgentAccessDenied(getComponent: () => React.ComponentType): () => React.ComponentType {
let ProtectedComponent: React.ComponentType | undefined;
return () => {
if (!ProtectedComponent) {
const Component = getComponent();
ProtectedComponent = (props) => (
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.AGENT]}>
<Component {...props} />
</DelegateNoAccessWrapper>
);
}
return ProtectedComponent;
};
}

export default withAgentAccessDenied;
66 changes: 40 additions & 26 deletions src/pages/settings/InitialSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {hasPendingExpensifyCardAction} from '@libs/CardUtils';
import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute';
import useIsSidebarRouteActive from '@libs/Navigation/helpers/useIsSidebarRouteActive';
import Navigation from '@libs/Navigation/Navigation';
import {useIsAgentAccount} from '@libs/SessionUtils';
import {getFreeTrialText, hasSubscriptionRedDotError} from '@libs/SubscriptionUtils';
import type {SkeletonSpanReasonAttributes} from '@libs/telemetry/useSkeletonSpan';
import {shouldHideOldAppRedirect} from '@libs/TryNewDotUtils';
Expand Down Expand Up @@ -170,6 +171,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
const [tryNewDot, tryNewDotMetadata] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT);
const isLoadingTryNewDot = isLoadingOnyxValue(tryNewDotMetadata);
const {isBetaEnabled} = usePermissions();
const isAgentAccount = useIsAgentAccount();

const freeTrialText = getFreeTrialText(currentUserPersonalDetails.accountID, translate, policies, introSelected, firstDayFreeTrial, lastDayFreeTrial);

Expand Down Expand Up @@ -261,47 +263,59 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.PROFILE,
action: () => Navigation.navigate(ROUTES.SETTINGS_PROFILE.getRoute()),
},
{
translationKey: 'common.wallet',
icon: icons.Wallet,
screenName: SCREENS.SETTINGS.WALLET.ROOT,
brickRoadIndicator: walletBrickRoadIndicator,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.WALLET,
action: () => Navigation.navigate(ROUTES.SETTINGS_WALLET),
badgeText: hasActivatedWallet ? convertToDisplayString(userWallet?.currentBalance, CONST.CURRENCY.USD) : undefined,
},
...(!isAgentAccount
? [
{
translationKey: 'common.wallet' as const,
icon: icons.Wallet,
screenName: SCREENS.SETTINGS.WALLET.ROOT,
brickRoadIndicator: walletBrickRoadIndicator,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.WALLET,
action: () => Navigation.navigate(ROUTES.SETTINGS_WALLET),
badgeText: hasActivatedWallet ? convertToDisplayString(userWallet?.currentBalance, CONST.CURRENCY.USD) : undefined,
},
]
: []),
{
translationKey: 'expenseRulesPage.title',
icon: icons.Bolt,
screenName: SCREENS.SETTINGS.RULES.ROOT,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.RULES,
action: () => Navigation.navigate(ROUTES.SETTINGS_RULES),
},
{
translationKey: 'common.preferences',
icon: icons.Gear,
screenName: SCREENS.SETTINGS.PREFERENCES.ROOT,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.PREFERENCES,
action: () => Navigation.navigate(ROUTES.SETTINGS_PREFERENCES),
},
...(!isAgentAccount
? [
{
translationKey: 'common.preferences' as const,
icon: icons.Gear,
screenName: SCREENS.SETTINGS.PREFERENCES.ROOT,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.PREFERENCES,
action: () => Navigation.navigate(ROUTES.SETTINGS_PREFERENCES),
Comment thread
NicolasBonet marked this conversation as resolved.
},
]
: []),
{
translationKey: 'delegate.copilot',
icon: icons.Users,
screenName: SCREENS.SETTINGS.COPILOT,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.COPILOT,
action: () => Navigation.navigate(ROUTES.SETTINGS_COPILOT),
},
{
translationKey: 'initialSettingsPage.security',
icon: icons.Lock,
screenName: SCREENS.SETTINGS.SECURITY,
brickRoadIndicator: securityBrickRoadIndicator,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.SECURITY,
action: () => Navigation.navigate(ROUTES.SETTINGS_SECURITY),
},
...(!isAgentAccount
? [
{
translationKey: 'initialSettingsPage.security' as const,
icon: icons.Lock,
screenName: SCREENS.SETTINGS.SECURITY,
brickRoadIndicator: securityBrickRoadIndicator,
sentryLabel: CONST.SENTRY_LABEL.ACCOUNT.SECURITY,
action: () => Navigation.navigate(ROUTES.SETTINGS_SECURITY),
},
]
: []),
];

if (isBetaEnabled(CONST.BETAS.CUSTOM_AGENT)) {
if (!isAgentAccount && isBetaEnabled(CONST.BETAS.CUSTOM_AGENT)) {
const rulesIndex = accountItems.findIndex((item) => item.screenName === SCREENS.SETTINGS.RULES.ROOT);
accountItems.splice(rulesIndex + 1, 0, {
translationKey: 'agentsPage.title',
Expand All @@ -314,7 +328,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
});
}

if (subscriptionPlan || (amountOwed ?? 0) > 0) {
if (!isAgentAccount && (subscriptionPlan || (amountOwed ?? 0) > 0)) {
accountItems.splice(1, 0, {
Comment thread
NicolasBonet marked this conversation as resolved.
translationKey: 'allSettingsScreen.subscription',
icon: icons.CreditCard,
Expand Down
Loading
Loading