Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -6304,6 +6304,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
85 changes: 44 additions & 41 deletions src/pages/settings/Agents/AgentsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React, {useEffect} from 'react';
import {FlatList, View} from 'react-native';
import Button from '@components/Button';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import GenericEmptyStateComponent from '@components/EmptyStateComponent/GenericEmptyStateComponent';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
Expand Down Expand Up @@ -97,48 +98,50 @@ function AgentsPage() {
}

return (
<ScreenWrapper
enableEdgeToEdgeBottomSafeAreaPadding
style={[styles.defaultModalContainer]}
testID={AgentsPage.displayName}
shouldShowOfflineIndicatorInWideScreen
shouldMobileOfflineIndicatorStickToBottom={false}
offlineIndicatorStyle={styles.mtAuto}
>
<HeaderWithBackButton
icon={illustrations.AiBot}
onBackButtonPress={() => Navigation.goBack()}
shouldShowBackButton={shouldUseNarrowLayout}
shouldUseHeadlineHeader
shouldDisplaySearchRouter
shouldDisplayHelpButton
title={translate('agentsPage.title')}
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.AGENT]}>
<ScreenWrapper
enableEdgeToEdgeBottomSafeAreaPadding
style={[styles.defaultModalContainer]}
testID={AgentsPage.displayName}
shouldShowOfflineIndicatorInWideScreen
shouldMobileOfflineIndicatorStickToBottom={false}
offlineIndicatorStyle={styles.mtAuto}
>
{!shouldUseNarrowLayout && newAgentButton}
</HeaderWithBackButton>
{shouldUseNarrowLayout && <View style={[styles.ph5, styles.pb3]}>{newAgentButton}</View>}
{hasAgents ? (
<>
<Text style={[styles.textSupporting, styles.ph5, styles.pb3, styles.pt3]}>{translate('agentsPage.subtitle')}</Text>
<FlatList
data={agentItems}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
</>
) : (
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.flexShrink0]}>
<GenericEmptyStateComponent
headerMedia={illustrations.TvScreenRobot}
title={translate('agentsPage.emptyAgents.title')}
subtitle={translate('agentsPage.emptyAgents.subtitle')}
subtitleStyles={styles.agentsPageEmptyStateSubtitle}
headerStyles={styles.emptyStateCardIllustrationContainer}
headerContentStyles={styles.agentsPageEmptyStateIllustration}
/>
</ScrollView>
)}
</ScreenWrapper>
<HeaderWithBackButton
icon={illustrations.AiBot}
onBackButtonPress={() => Navigation.goBack()}
shouldShowBackButton={shouldUseNarrowLayout}
shouldUseHeadlineHeader
shouldDisplaySearchRouter
shouldDisplayHelpButton
title={translate('agentsPage.title')}
>
{!shouldUseNarrowLayout && newAgentButton}
</HeaderWithBackButton>
{shouldUseNarrowLayout && <View style={[styles.ph5, styles.pb3]}>{newAgentButton}</View>}
{hasAgents ? (
<>
<Text style={[styles.textSupporting, styles.ph5, styles.pb3, styles.pt3]}>{translate('agentsPage.subtitle')}</Text>
<FlatList
data={agentItems}
renderItem={renderItem}
keyExtractor={keyExtractor}
/>
</>
) : (
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.flexShrink0]}>
<GenericEmptyStateComponent
headerMedia={illustrations.TvScreenRobot}
title={translate('agentsPage.emptyAgents.title')}
subtitle={translate('agentsPage.emptyAgents.subtitle')}
subtitleStyles={styles.agentsPageEmptyStateSubtitle}
headerStyles={styles.emptyStateCardIllustrationContainer}
headerContentStyles={styles.agentsPageEmptyStateIllustration}
/>
</ScrollView>
)}
</ScreenWrapper>
</DelegateNoAccessWrapper>
);
}

Expand Down
46 changes: 28 additions & 18 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 @@ -166,6 +167,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 @@ -257,29 +259,37 @@ 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: 'initialSettingsPage.security',
icon: icons.Lock,
Expand All @@ -290,7 +300,7 @@ function InitialSettingsPage({currentUserPersonalDetails}: InitialSettingsPagePr
},
];

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 @@ -302,7 +312,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
38 changes: 21 additions & 17 deletions src/pages/settings/Preferences/LanguagePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, {useMemo, useRef} from 'react';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
Expand All @@ -8,6 +9,7 @@ import type {ListItem} from '@components/SelectionList/ListItem/types';
import useLocalize from '@hooks/useLocalize';
import Navigation from '@libs/Navigation/Navigation';
import {setLocale} from '@userActions/App';
import CONST from '@src/CONST';
import {LOCALE_TO_LANGUAGE_STRING, SORTED_LOCALES} from '@src/CONST/LOCALES';
import type Locale from '@src/types/onyx/Locale';

Expand Down Expand Up @@ -43,24 +45,26 @@ function LanguagePage() {
};

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID="LanguagePage"
>
<HeaderWithBackButton
title={translate('languagePage.language')}
onBackButtonPress={() => Navigation.goBack()}
/>
<FullPageOfflineBlockingView>
<SelectionList
data={locales}
ListItem={SingleSelectListItem}
onSelectRow={updateLanguage}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={locales.find((locale) => locale.isSelected)?.keyForList}
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.AGENT]}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID="LanguagePage"
>
<HeaderWithBackButton
title={translate('languagePage.language')}
onBackButtonPress={() => Navigation.goBack()}
/>
</FullPageOfflineBlockingView>
</ScreenWrapper>
<FullPageOfflineBlockingView>
<SelectionList
data={locales}
ListItem={SingleSelectListItem}
onSelectRow={updateLanguage}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={locales.find((locale) => locale.isSelected)?.keyForList}
/>
</FullPageOfflineBlockingView>
</ScreenWrapper>
</DelegateNoAccessWrapper>
);
}

Expand Down
57 changes: 30 additions & 27 deletions src/pages/settings/Preferences/PaymentCurrencyPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import CurrencySelectionList from '@components/CurrencySelectionList';
import type {CurrencyListItem} from '@components/CurrencySelectionList/types';
import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
Expand Down Expand Up @@ -29,35 +30,37 @@ function PaymentCurrencyPage() {
};

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID="PaymentCurrencyPage"
>
{({didScreenTransitionEnd}) => (
<>
<HeaderWithBackButton
title={translate('billingCurrency.paymentCurrency')}
shouldShowBackButton
onBackButtonPress={handleDismissKeyboardAndGoBack}
/>
<DelegateNoAccessWrapper accessDeniedVariants={[CONST.DELEGATE.DENIED_ACCESS_VARIANTS.AGENT]}>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID="PaymentCurrencyPage"
>
{({didScreenTransitionEnd}) => (
<>
<HeaderWithBackButton
title={translate('billingCurrency.paymentCurrency')}
shouldShowBackButton
onBackButtonPress={handleDismissKeyboardAndGoBack}
/>

<Text style={[styles.mh5, styles.mv4]}>{translate('billingCurrency.paymentCurrencyDescription')}</Text>
<Text style={[styles.mh5, styles.mv4]}>{translate('billingCurrency.paymentCurrencyDescription')}</Text>

<CurrencySelectionList
recentlyUsedCurrencies={[]}
searchInputLabel={translate('common.search')}
onSelect={(option: CurrencyListItem) => {
if (option.currencyCode !== paymentCurrency) {
updateGeneralSettings(personalPolicy, personalPolicy?.name ?? '', option.currencyCode);
}
handleDismissKeyboardAndGoBack();
}}
initiallySelectedCurrencyCode={paymentCurrency}
didScreenTransitionEnd={didScreenTransitionEnd}
/>
</>
)}
</ScreenWrapper>
<CurrencySelectionList
recentlyUsedCurrencies={[]}
searchInputLabel={translate('common.search')}
onSelect={(option: CurrencyListItem) => {
if (option.currencyCode !== paymentCurrency) {
updateGeneralSettings(personalPolicy, personalPolicy?.name ?? '', option.currencyCode);
}
handleDismissKeyboardAndGoBack();
}}
initiallySelectedCurrencyCode={paymentCurrency}
didScreenTransitionEnd={didScreenTransitionEnd}
/>
</>
)}
</ScreenWrapper>
</DelegateNoAccessWrapper>
);
}

Expand Down
Loading
Loading