diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index f2af8e9330ba..43f9a4bc280a 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -697,6 +697,9 @@ const ONYXKEYS = {
/** Stores the information about the state of issuing a new card */
ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard_',
+
+ /** Used for identifying user as admin of a domain */
+ SHARED_NVP_PRIVATE_ADMIN_ACCESS: 'sharedNVP_private_admin_access_',
},
/** List of Form ids */
@@ -1081,6 +1084,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID;
[ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist;
[ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
+ [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean;
};
type OnyxValuesMapping = {
diff --git a/src/components/Domain/DomainsListRow.tsx b/src/components/Domain/DomainsListRow.tsx
new file mode 100644
index 000000000000..79cecbc180f9
--- /dev/null
+++ b/src/components/Domain/DomainsListRow.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import {View} from 'react-native';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import TextWithTooltip from '@components/TextWithTooltip';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+
+type DomainsListRowProps = {
+ /** Name of the domain */
+ title: string;
+
+ /** Whether the row is hovered, so we can modify its style */
+ isHovered: boolean;
+
+ /** Whether the icon at the end of the row should be displayed */
+ shouldShowRightIcon: boolean;
+};
+
+function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+
+ return (
+
+
+
+
+
+
+ {shouldShowRightIcon && (
+
+
+
+ )}
+
+ );
+}
+
+DomainsListRow.displayName = 'DomainsListRow';
+
+export default DomainsListRow;
diff --git a/src/languages/de.ts b/src/languages/de.ts
index b0c96cfabd9d..12949c72e071 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -673,6 +673,7 @@ const translations = {
pinned: 'Angeheftet',
read: 'Gelesen',
copyToClipboard: 'In die Zwischenablage kopieren',
+ domains: 'Domänen',
},
supportalNoAccess: {
title: 'Nicht so schnell',
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 01c577acb605..947eb5515c94 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -666,6 +666,7 @@ const translations = {
pinned: 'Pinned',
read: 'Read',
copyToClipboard: 'Copy to clipboard',
+ domains: 'Domains',
},
supportalNoAccess: {
title: 'Not so fast',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 85ad0ee72c50..7df324308ab5 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -652,6 +652,7 @@ const translations = {
pinned: 'Fijado',
read: 'Leído',
copyToClipboard: 'Copiar al portapapeles',
+ domains: 'Dominios',
},
supportalNoAccess: {
title: 'No tan rápido',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 4d3f16b2544c..efc1aedd9611 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -671,6 +671,7 @@ const translations = {
pinned: 'Épinglé',
read: 'Lu',
copyToClipboard: 'Copier dans le presse-papiers',
+ domains: 'Domaines',
},
supportalNoAccess: {
title: 'Pas si vite',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index e975417f7f45..7ebd0283f29f 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -671,6 +671,7 @@ const translations = {
pinned: 'Fissato',
read: 'Letto',
copyToClipboard: 'Copia negli appunti',
+ domains: 'Domini',
},
supportalNoAccess: {
title: 'Non così in fretta',
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 84b9cfe017a2..f2186339f478 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -671,6 +671,7 @@ const translations = {
pinned: '固定済み',
read: '既読',
copyToClipboard: 'クリップボードにコピー',
+ domains: 'ドメイン',
},
supportalNoAccess: {
title: 'ちょっと待ってください',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index fa13aaaa535b..a46cf568be9c 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -670,6 +670,7 @@ const translations = {
pinned: 'Vastgezet',
read: 'Gelezen',
copyToClipboard: 'Kopiëren naar klembord',
+ domains: 'Domeinen',
},
supportalNoAccess: {
title: 'Niet zo snel',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 67e4b21a96ec..fa977cec4c74 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -671,6 +671,7 @@ const translations = {
pinned: 'Przypięte',
read: 'Przeczytane',
copyToClipboard: 'Skopiuj do schowka',
+ domains: 'Domeny',
},
supportalNoAccess: {
title: 'Nie tak szybko',
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index cdee8c9cbe63..5219b7edb6bf 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -673,6 +673,7 @@ const translations = {
pinned: 'Fixado',
read: 'Lido',
copyToClipboard: 'Copiar para a área de transferência',
+ domains: 'Domínios',
},
supportalNoAccess: {
title: 'Não tão rápido',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 69fb7d33a47b..495e9fa108f5 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -672,6 +672,7 @@ const translations = {
pinned: '已固定',
read: '已读',
copyToClipboard: '复制到剪贴板',
+ domains: '域名',
},
supportalNoAccess: {
title: '慢一点',
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index a2f45e492e21..e337409c4ee1 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -1,10 +1,12 @@
import {useIsFocused, useRoute} from '@react-navigation/native';
+import {Str} from 'expensify-common';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {FlatList, InteractionManager, View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import Button from '@components/Button';
import ConfirmModal from '@components/ConfirmModal';
+import DomainsListRow from '@components/Domain/DomainsListRow';
import EmptyStateComponent from '@components/EmptyStateComponent';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import * as Expensicons from '@components/Icon/Expensicons';
@@ -38,6 +40,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useTransactionViolationOfWorkspace from '@hooks/useTransactionViolationOfWorkspace';
import {isConnectionInProgress} from '@libs/actions/connections';
+import {openOldDotLink} from '@libs/actions/Link';
import {clearWorkspaceOwnerChangeFlow, requestWorkspaceOwnerChange} from '@libs/actions/Policy/Member';
import {calculateBillNewDot, clearDeleteWorkspaceError, clearDuplicateWorkspace, clearErrors, deleteWorkspace, leaveWorkspace, removeWorkspace} from '@libs/actions/Policy/Policy';
import {callFunctionIfActionIsAllowed} from '@libs/actions/Session';
@@ -75,7 +78,7 @@ import type {PolicyDetailsForNonMembers} from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import WorkspacesListRow from './WorkspacesListRow';
-type WorkspaceItem = ListItem &
+type WorkspaceItem = {listItemType: 'workspace'} & ListItem &
Required> &
Pick &
Pick &
@@ -87,9 +90,11 @@ type WorkspaceItem = ListItem &
policyID?: string;
isJoinRequestPending?: boolean;
};
+type DomainItem = {listItemType: 'domain'; title: string; action: () => void; disabled: boolean} & Pick;
+type WorkspaceOrDomainListItem = WorkspaceItem | DomainItem | {listItemType: 'domains-header' | 'workspaces-empty-state'};
-// eslint-disable-next-line react/no-unused-prop-types
-type GetMenuItem = {item: WorkspaceItem; index: number};
+type GetWorkspaceMenuItem = {item: WorkspaceItem; index: number};
+type GetDomainMenuItem = {item: DomainItem; index: number};
/**
* Dismisses the errors on one item
@@ -130,6 +135,9 @@ function WorkspacesListPage() {
const {isRestrictedToPreferredPolicy} = usePreferredPolicy();
const [reimbursementAccountError] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {canBeMissing: true, selector: reimbursementAccountErrorSelector});
+ const [allDomains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN, {canBeMissing: false});
+ const [adminAccess] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS, {canBeMissing: false});
+
// This hook preloads the screens of adjacent tabs to make changing tabs faster.
usePreloadFullScreenNavigators();
@@ -248,8 +256,8 @@ function WorkspacesListPage() {
/**
* Gets the menu item for each workspace
*/
- const getMenuItem = useCallback(
- ({item, index}: GetMenuItem) => {
+ const getWorkspaceMenuItem = useCallback(
+ ({item, index}: GetWorkspaceMenuItem) => {
const isAdmin = isPolicyAdmin(item as unknown as PolicyType, session?.email);
const isOwner = item.ownerAccountID === session?.accountID;
const isDefault = activePolicyID === item.policyID;
@@ -378,11 +386,7 @@ function WorkspacesListPage() {
translate,
policies,
fundList,
- styles.ph5,
- styles.mb2,
- styles.mh5,
- styles.hoveredComponentBG,
- styles.offlineFeedbackDeleted,
+ styles,
loadingSpinnerIconIndex,
shouldCalculateBillNewDot,
setIsDeletingPaidWorkspace,
@@ -395,6 +399,36 @@ function WorkspacesListPage() {
],
);
+ /**
+ * Gets the menu item for each domain
+ */
+ const getDomainMenuItem = useCallback(
+ ({item, index}: GetDomainMenuItem) => (
+
+
+ {({hovered}) => (
+
+ )}
+
+
+ ),
+ [styles],
+ );
+
const navigateToWorkspace = useCallback(
(policyID: string) => {
// On the wide layout, we always want to open the Profile page when opening workspace settings from the list
@@ -407,6 +441,8 @@ function WorkspacesListPage() {
[shouldUseNarrowLayout],
);
+ const navigateToDomain = useCallback(() => openOldDotLink(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL), []);
+
/**
* Add free policies (workspaces) to the list of menu items and returns the list of menu items
*/
@@ -425,6 +461,7 @@ function WorkspacesListPage() {
const policyInfo = Object.values(policy.policyDetailsForNonMembers).at(0) as PolicyDetailsForNonMembers;
const id = Object.keys(policy.policyDetailsForNonMembers).at(0);
return {
+ listItemType: 'workspace',
title: policyInfo.name,
icon: policyInfo?.avatar ? policyInfo.avatar : getDefaultWorkspaceAvatar(policy.name),
disabled: true,
@@ -442,6 +479,7 @@ function WorkspacesListPage() {
};
}
return {
+ listItemType: 'workspace',
title: policy.name,
icon: policy.avatarURL ? policy.avatarURL : getDefaultWorkspaceAvatar(policy.name),
action: () => navigateToWorkspace(policy.id),
@@ -473,6 +511,28 @@ function WorkspacesListPage() {
const sortWorkspace = useCallback((workspaceItems: WorkspaceItem[]) => workspaceItems.sort((a, b) => localeCompare(a.title, b.title)), [localeCompare]);
const [inputValue, setInputValue, filteredWorkspaces] = useSearchResults(workspaces, filterWorkspace, sortWorkspace);
+ const domains = useMemo(() => {
+ if (!allDomains) {
+ return [];
+ }
+
+ return Object.values(allDomains).reduce((domainItems, domain) => {
+ if (!domain) {
+ return domainItems;
+ }
+
+ domainItems.push({
+ listItemType: 'domain',
+ title: Str.extractEmailDomain(domain.email),
+ action: navigateToDomain,
+ disabled: !adminAccess?.[`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domain.accountID}`],
+ pendingAction: domain.pendingAction,
+ });
+
+ return domainItems;
+ }, []);
+ }, [navigateToDomain, allDomains, adminAccess]);
+
useEffect(() => {
if (isEmptyObject(duplicateWorkspace) || !filteredWorkspaces.length || !isFocused) {
return;
@@ -530,14 +590,39 @@ function WorkspacesListPage() {
>
);
- const getHeaderButton = () => (
-