diff --git a/assets/images/caret-up-down.svg b/assets/images/caret-up-down.svg
new file mode 100644
index 000000000000..d08aa2a1ebbd
--- /dev/null
+++ b/assets/images/caret-up-down.svg
@@ -0,0 +1,17 @@
+
+
\ No newline at end of file
diff --git a/src/CONST.ts b/src/CONST.ts
index 30b0b5364d83..be14ba241404 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -387,6 +387,7 @@ const CONST = {
REPORT_FIELDS_FEATURE: 'reportFieldsFeature',
WORKSPACE_FEEDS: 'workspaceFeeds',
NETSUITE_USA_TAX: 'netsuiteUsaTax',
+ NEW_DOT_COPILOT: 'newDotCopilot',
WORKSPACE_RULES: 'workspaceRules',
COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit',
},
@@ -3877,6 +3878,10 @@ const CONST = {
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
},
+ DELEGATE_ROLE: {
+ SUBMITTER: 'submitter',
+ ALL: 'all',
+ },
STRIPE_GBP_AUTH_STATUSES: {
SUCCEEDED: 'succeeded',
CARD_AUTHENTICATION_REQUIRED: 'authentication_required',
diff --git a/src/components/AccountSwitcher.tsx b/src/components/AccountSwitcher.tsx
new file mode 100644
index 000000000000..ba30ea0062b9
--- /dev/null
+++ b/src/components/AccountSwitcher.tsx
@@ -0,0 +1,202 @@
+import React, {useRef, useState} from 'react';
+import {View} from 'react-native';
+import {useOnyx} from 'react-native-onyx';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import usePermissions from '@hooks/usePermissions';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {clearDelegatorErrors, connect, disconnect} from '@libs/actions/Delegate';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import variables from '@styles/variables';
+import * as Modal from '@userActions/Modal';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {PersonalDetails} from '@src/types/onyx';
+import Avatar from './Avatar';
+import ConfirmModal from './ConfirmModal';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import type {MenuItemProps} from './MenuItem';
+import MenuItemList from './MenuItemList';
+import type {MenuItemWithLink} from './MenuItemList';
+import Popover from './Popover';
+import {PressableWithFeedback} from './Pressable';
+import Text from './Text';
+
+function AccountSwitcher() {
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
+ const {canUseNewDotCopilot} = usePermissions();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const [account] = useOnyx(ONYXKEYS.ACCOUNT);
+ const buttonRef = useRef(null);
+
+ const [shouldShowDelegatorMenu, setShouldShowDelegatorMenu] = useState(false);
+ const [shouldShowOfflineModal, setShouldShowOfflineModal] = useState(false);
+ const delegators = account?.delegatedAccess?.delegators ?? [];
+
+ const isActingAsDelegate = !!account?.delegatedAccess?.delegate ?? false;
+ const canSwitchAccounts = canUseNewDotCopilot && (delegators.length > 0 || isActingAsDelegate);
+
+ const createBaseMenuItem = (personalDetails: PersonalDetails | undefined, error?: TranslationPaths, additionalProps: MenuItemWithLink = {}): MenuItemWithLink => {
+ return {
+ title: personalDetails?.displayName ?? personalDetails?.login,
+ description: personalDetails?.login,
+ avatarID: personalDetails?.accountID ?? -1,
+ icon: personalDetails?.avatar ?? '',
+ iconType: CONST.ICON_TYPE_AVATAR,
+ outerWrapperStyle: shouldUseNarrowLayout ? {} : styles.accountSwitcherPopover,
+ numberOfLinesDescription: 1,
+ errorText: error ? translate(error) : '',
+ shouldShowRedDotIndicator: !!error,
+ errorTextStyle: styles.mt2,
+ ...additionalProps,
+ };
+ };
+
+ const menuItems = (): MenuItemProps[] => {
+ const currentUserMenuItem = createBaseMenuItem(currentUserPersonalDetails, undefined, {
+ wrapperStyle: [styles.buttonDefaultBG],
+ focused: true,
+ shouldShowRightIcon: true,
+ iconRight: Expensicons.Checkmark,
+ success: true,
+ key: `${currentUserPersonalDetails?.login}-current`,
+ });
+
+ if (isActingAsDelegate) {
+ const delegateEmail = account?.delegatedAccess?.delegate ?? '';
+
+ // Avoid duplicating the current user in the list when switching accounts
+ if (delegateEmail === currentUserPersonalDetails.login) {
+ return [currentUserMenuItem];
+ }
+
+ const delegatePersonalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(delegateEmail);
+ const error = account?.delegatedAccess?.error;
+
+ return [
+ createBaseMenuItem(delegatePersonalDetails, error, {
+ onPress: () => {
+ if (isOffline) {
+ Modal.close(() => setShouldShowOfflineModal(true));
+ return;
+ }
+ disconnect();
+ },
+ key: `${delegateEmail}-delegate`,
+ }),
+ currentUserMenuItem,
+ ];
+ }
+
+ const delegatorMenuItems: MenuItemProps[] = delegators
+ .filter(({email}) => email !== currentUserPersonalDetails.login)
+ .map(({email, role, error}, index) => {
+ const personalDetails = PersonalDetailsUtils.getPersonalDetailByEmail(email);
+ return createBaseMenuItem(personalDetails, error, {
+ badgeText: translate('delegate.role', role),
+ onPress: () => {
+ if (isOffline) {
+ Modal.close(() => setShouldShowOfflineModal(true));
+ return;
+ }
+ connect(email);
+ },
+ key: `${email}-${index}`,
+ });
+ });
+
+ return [currentUserMenuItem, ...delegatorMenuItems];
+ };
+
+ return (
+ <>
+ {
+ setShouldShowDelegatorMenu(!shouldShowDelegatorMenu);
+ }}
+ ref={buttonRef}
+ interactive={canSwitchAccounts}
+ wrapperStyle={[styles.flexGrow1, styles.flex1, styles.mnw0, styles.justifyContentCenter]}
+ >
+
+
+
+
+
+ {currentUserPersonalDetails?.displayName}
+
+ {canSwitchAccounts && (
+
+
+
+ )}
+
+
+ {currentUserPersonalDetails?.login}
+
+
+
+
+ {canSwitchAccounts && (
+ {
+ setShouldShowDelegatorMenu(false);
+ clearDelegatorErrors();
+ }}
+ anchorRef={buttonRef}
+ anchorPosition={styles.accountSwitcherAnchorPosition}
+ >
+
+ {translate('delegate.switchAccount')}
+
+
+
+ )}
+ setShouldShowOfflineModal(false)}
+ onCancel={() => setShouldShowOfflineModal(false)}
+ confirmText={translate('common.buttonConfirm')}
+ prompt={translate('common.offlinePrompt')}
+ shouldShowCancelButton={false}
+ />
+ >
+ );
+}
+
+AccountSwitcher.displayName = 'AccountSwitcher';
+
+export default AccountSwitcher;
diff --git a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx b/src/components/AccountSwitcherSkeletonView/index.tsx
similarity index 50%
rename from src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx
rename to src/components/AccountSwitcherSkeletonView/index.tsx
index 21e82c26f769..eb01a23e9ade 100644
--- a/src/components/CurrentUserPersonalDetailsSkeletonView/index.tsx
+++ b/src/components/AccountSwitcherSkeletonView/index.tsx
@@ -6,10 +6,9 @@ import SkeletonViewContentLoader from '@components/SkeletonViewContentLoader';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import variables from '@styles/variables';
import CONST from '@src/CONST';
-type CurrentUserPersonalDetailsSkeletonViewProps = {
+type AccountSwitcherSkeletonViewProps = {
/** Whether to animate the skeleton view */
shouldAnimate?: boolean;
@@ -17,45 +16,43 @@ type CurrentUserPersonalDetailsSkeletonViewProps = {
avatarSize?: ValueOf;
};
-function CurrentUserPersonalDetailsSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.LARGE}: CurrentUserPersonalDetailsSkeletonViewProps) {
+function AccountSwitcherSkeletonView({shouldAnimate = true, avatarSize = CONST.AVATAR_SIZE.LARGE}: AccountSwitcherSkeletonViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const avatarPlaceholderSize = StyleUtils.getAvatarSize(avatarSize);
const avatarPlaceholderRadius = avatarPlaceholderSize / 2;
- const spaceBetweenAvatarAndHeadline = styles.mb3.marginBottom + styles.mt1.marginTop + (variables.lineHeightXXLarge - variables.fontSizeXLarge) / 2;
- const headlineSize = variables.fontSizeXLarge;
- const spaceBetweenHeadlineAndLabel = styles.mt1.marginTop + (variables.lineHeightXXLarge - variables.fontSizeXLarge) / 2;
- const labelSize = variables.fontSizeLabel;
+ const startPositionX = 30;
+
return (
);
}
-CurrentUserPersonalDetailsSkeletonView.displayName = 'CurrentUserPersonalDetailsSkeletonView';
-export default CurrentUserPersonalDetailsSkeletonView;
+AccountSwitcherSkeletonView.displayName = 'AccountSwitcherSkeletonView';
+export default AccountSwitcherSkeletonView;
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index b1adf360bae6..9d59accb498d 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -33,6 +33,7 @@ import Calendar from '@assets/images/calendar.svg';
import Camera from '@assets/images/camera.svg';
import CarWithKey from '@assets/images/car-with-key.svg';
import Car from '@assets/images/car.svg';
+import CaretUpDown from '@assets/images/caret-up-down.svg';
import Cash from '@assets/images/cash.svg';
import Chair from '@assets/images/chair.svg';
import ChatBubbleAdd from '@assets/images/chatbubble-add.svg';
@@ -387,5 +388,6 @@ export {
Filters,
CalendarSolid,
Filter,
+ CaretUpDown,
Feed,
};
diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx
index 4ba9260e23ff..c03bd712d74c 100644
--- a/src/components/MenuItemList.tsx
+++ b/src/components/MenuItemList.tsx
@@ -11,6 +11,9 @@ type MenuItemLink = string | (() => Promise);
type MenuItemWithLink = MenuItemProps & {
/** The link to open when the menu item is clicked */
link?: MenuItemLink;
+
+ /** A unique key for the menu item */
+ key?: string;
};
type MenuItemListProps = {
@@ -43,7 +46,7 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuIt
<>
{menuItems.map((menuItemProps) => (