diff --git a/assets/images/simple-illustrations/simple-illustration__approval-members.svg b/assets/images/simple-illustrations/simple-illustration__approval-members.svg
new file mode 100644
index 0000000000000..5e0837a3e14fc
--- /dev/null
+++ b/assets/images/simple-illustrations/simple-illustration__approval-members.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts
index 700a4e3affb4b..2ad248fa1c63d 100755
--- a/src/ONYXKEYS.ts
+++ b/src/ONYXKEYS.ts
@@ -731,6 +731,9 @@ const ONYXKEYS = {
/** SAML login metadata for a domain */
SAML_METADATA: 'saml_metadata_',
+
+ /** Stores domain admin account ID */
+ EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_',
},
/** List of Form ids */
@@ -1117,6 +1120,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean;
[ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata;
+ [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number;
};
type OnyxValuesMapping = {
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index 8621adc029be5..f56f6a9d7815a 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -3421,38 +3421,42 @@ const ROUTES = {
getRoute: (backTo?: string) => getUrlWithBackToParam('test-tools' as const, backTo),
},
WORKSPACES_VERIFY_DOMAIN: {
- route: 'workspaces/verify-domain/:accountID',
- getRoute: (accountID: number) => `workspaces/verify-domain/${accountID}` as const,
+ route: 'workspaces/verify-domain/:domainAccountID',
+ getRoute: (domainAccountID: number) => `workspaces/verify-domain/${domainAccountID}` as const,
},
WORKSPACES_DOMAIN_VERIFIED: {
- route: 'workspaces/domain-verified/:accountID',
- getRoute: (accountID: number) => `workspaces/domain-verified/${accountID}` as const,
+ route: 'workspaces/domain-verified/:domainAccountID',
+ getRoute: (domainAccountID: number) => `workspaces/domain-verified/${domainAccountID}` as const,
},
WORKSPACES_ADD_DOMAIN: 'workspaces/add-domain',
WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT: `workspaces/add-domain/${VERIFY_ACCOUNT}`,
WORKSPACES_DOMAIN_ADDED: {
- route: 'workspaces/domain-added/:accountID',
- getRoute: (accountID: number) => `workspaces/domain-added/${accountID}` as const,
+ route: 'workspaces/domain-added/:domainAccountID',
+ getRoute: (domainAccountID: number) => `workspaces/domain-added/${domainAccountID}` as const,
},
WORKSPACES_DOMAIN_ACCESS_RESTRICTED: {
- route: 'workspaces/domain-access-restricted/:accountID',
- getRoute: (accountID: number) => `workspaces/domain-access-restricted/${accountID}` as const,
+ route: 'workspaces/domain-access-restricted/:domainAccountID',
+ getRoute: (domainAccountID: number) => `workspaces/domain-access-restricted/${domainAccountID}` as const,
},
DOMAIN_INITIAL: {
- route: 'domain/:accountID',
- getRoute: (accountID: number) => `domain/${accountID}` as const,
+ route: 'domain/:domainAccountID',
+ getRoute: (domainAccountID: number) => `domain/${domainAccountID}` as const,
},
DOMAIN_SAML: {
- route: 'domain/:accountID/saml',
- getRoute: (accountID: number) => `domain/${accountID}/saml` as const,
+ route: 'domain/:domainAccountID/saml',
+ getRoute: (domainAccountID: number) => `domain/${domainAccountID}/saml` as const,
},
DOMAIN_VERIFY: {
- route: 'domain/:accountID/verify',
- getRoute: (accountID: number) => `domain/${accountID}/verify` as const,
+ route: 'domain/:domainAccountID/verify',
+ getRoute: (domainAccountID: number) => `domain/${domainAccountID}/verify` as const,
},
DOMAIN_VERIFIED: {
- route: 'domain/:accountID/verified',
- getRoute: (accountID: number) => `domain/${accountID}/verified` as const,
+ route: 'domain/:domainAccountID/verified',
+ getRoute: (domainAccountID: number) => `domain/${domainAccountID}/verified` as const,
+ },
+ DOMAIN_ADMINS: {
+ route: 'domain/:domainAccountID/admins',
+ getRoute: (domainAccountID: number) => `domain/${domainAccountID}/admins` as const,
},
} as const;
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 24092ca91a3cd..0dbae61f5fec9 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -848,6 +848,7 @@ const SCREENS = {
VERIFIED: 'Domain_Verified',
INITIAL: 'Domain_Initial',
SAML: 'Domain_SAML',
+ ADMINS: 'Domain_Admins',
},
} as const;
diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts
index fa28809fadb68..7c685464478a4 100644
--- a/src/components/Icon/chunks/illustrations.chunk.ts
+++ b/src/components/Icon/chunks/illustrations.chunk.ts
@@ -79,6 +79,7 @@ import Abacus from '@assets/images/simple-illustrations/simple-illustration__aba
// Simple Illustrations - Original core ones
import Accounting from '@assets/images/simple-illustrations/simple-illustration__accounting.svg';
import Alert from '@assets/images/simple-illustrations/simple-illustration__alert.svg';
+import Members from '@assets/images/simple-illustrations/simple-illustration__approval-members.svg';
import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg';
import Binoculars from '@assets/images/simple-illustrations/simple-illustration__binoculars.svg';
import BlueShield from '@assets/images/simple-illustrations/simple-illustration__blueshield.svg';
@@ -315,6 +316,7 @@ const Illustrations = {
Mailbox,
ShieldYellow,
Clock,
+ Members,
};
/**
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index f33d4dab019dd..4ba13eb31ed0c 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -88,7 +88,7 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Inbox', 'MoneySearch', 'Buildings'] as const);
const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined;
- const paramsDomainAccountID = params && 'accountID' in params ? params.accountID : undefined;
+ const paramsDomainAccountID = params && 'domainAccountID' in params ? params.domainAccountID : undefined;
const lastViewedPolicySelector = useCallback(
(policies: OnyxCollection) => {
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 438a7f79fc6f0..33fbbad21c08f 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -8001,6 +8001,7 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
subtitle: 'Erzwingen Sie für Mitglieder Ihrer Domain die Anmeldung per Single Sign-On, schränken Sie die Erstellung von Workspaces ein und vieles mehr.',
enable: 'Aktivieren',
},
+ admins: {title: 'Admins', findAdmin: 'Admin finden'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 9eafd177a7046..73038db3eb245 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7845,6 +7845,10 @@ const translations = {
subtitle: 'Require members on your domain to log in via single sign-on, restrict workspace creation, and more.',
enable: 'Enable',
},
+ admins: {
+ title: 'Admins',
+ findAdmin: 'Find admin',
+ },
},
};
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c6810a535868f..aecf6bc201c5f 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -7941,6 +7941,10 @@ ${amount} para ${merchant} - ${date}`,
subtitle: 'Solicita que los miembros de tu dominio inicien sesión mediante inicio de sesión único, restringe la creación de espacios de trabajo y más.',
enable: 'Habilitar',
},
+ admins: {
+ title: 'Administradores',
+ findAdmin: 'Encontrar administrador',
+ },
},
};
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index f9b578835ca54..e21b600f9db91 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -8005,6 +8005,7 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
subtitle: "Exiger que les membres de votre domaine se connectent via l'authentification unique, restreindre la création d'espaces de travail, et plus encore.",
enable: 'Activer',
},
+ admins: {title: 'Admins', findAdmin: 'Trouver un admin'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/it.ts b/src/languages/it.ts
index a169b1e947130..06c38bcdab102 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7980,6 +7980,7 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
subtitle: 'Richiedi ai membri del tuo dominio di accedere tramite Single Sign-On, limita la creazione di spazi di lavoro e altro ancora.',
enable: 'Abilita',
},
+ admins: {title: 'Amministratori', findAdmin: 'Trova amministratore'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 52d58496a89bc..439e4d9eb3193 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7922,6 +7922,7 @@ Expensify の使い方をお見せするための*テストレシート*がこ
subtitle: 'ドメインのメンバーにシングルサインオンでのログインを必須化し、ワークスペースの作成を制限するなど、さらに多くのことができます。',
enable: '有効にする',
},
+ admins: {title: '管理者', findAdmin: '管理者を検索'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index e205c3130aff1..8ee1f5b729d1f 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7962,6 +7962,7 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`,
subtitle: 'Verplicht leden van je domein om in te loggen via single sign-on, beperk het aanmaken van werkruimten en meer.',
enable: 'Inschakelen',
},
+ admins: {title: 'Beheerders', findAdmin: 'Beheerder zoeken'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index da8ef238832d6..dfbd08e3f5a3d 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7950,6 +7950,7 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`,
subtitle: 'Wymagaj, aby członkowie Twojej domeny logowali się przez Single Sign-On (SSO), ograniczaj tworzenie obszarów roboczych i nie tylko.',
enable: 'Włącz',
},
+ admins: {title: 'Administratorzy', findAdmin: 'Znajdź administratora'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index e827ad66059f0..3e7dfed5f27ec 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7955,6 +7955,7 @@ Aqui está um *recibo de teste* para mostrar como funciona:`,
subtitle: 'Exija que os membros do seu domínio façam login por meio de logon único (SSO), restrinja a criação de espaços de trabalho e muito mais.',
enable: 'Ativar',
},
+ admins: {title: 'Administradores', findAdmin: 'Encontrar administrador'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 0ac0fd5f87e7a..5af51c44e5739 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -7786,6 +7786,7 @@ ${reportName}
addDomain: {title: '添加域', subtitle: '请输入您想访问的私有域名(例如:expensify.com)。', domainName: '域名', newDomain: '新域名'},
domainAdded: {title: '已添加域名', description: '接下来,您需要验证域名的所有权并调整您的安全设置。', configure: '配置'},
enhancedSecurity: {title: '增强的安全性', subtitle: '要求您域内的成员使用单点登录登录、限制工作区创建等。', enable: '启用'},
+ admins: {title: '管理员', findAdmin: '查找管理员'},
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx
index 81d1819ec2207..c7d883abc54cf 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx
@@ -13,6 +13,7 @@ import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
const loadDomainInitialPage = () => require('../../../../pages/domain/DomainInitialPage').default;
const loadDomainSamlPage = () => require('../../../../pages/domain/DomainSamlPage').default;
+const loadDomainAdminsPage = () => require('../../../../pages/domain/Admins/DomainAdminsPage').default;
const Split = createSplitNavigator();
@@ -43,6 +44,12 @@ function DomainSplitNavigator({route, navigation}: PlatformStackScreenProps
+
+
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
index 358b8ab189e97..b9a8ad120b0c3 100644
--- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers.ts
@@ -107,7 +107,7 @@ function handleOpenDomainSplitAction(
const actionToPushDomainSplitNavigator = StackActions.push(NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR, {
screen: action.payload.screenName,
params: {
- accountID: action.payload.accountID,
+ domainAccountID: action.payload.domainAccountID,
},
});
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
index 72d4c5c330d58..18bf27cc4a3bd 100644
--- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
@@ -22,7 +22,7 @@ type RootStackNavigatorActionType =
| {
type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_DOMAIN_SPLIT;
payload: {
- accountID: number;
+ domainAccountID: number;
screenName: DomainScreenName;
};
}
diff --git a/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts b/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
index 77e28c8be21f2..df07d7afcc293 100644
--- a/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
+++ b/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
@@ -100,7 +100,7 @@ const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, poli
return navigationRef.dispatch({
type: CONST.NAVIGATION.ACTION_TYPE.OPEN_DOMAIN_SPLIT,
- payload: {accountID: domain.accountID, screenName: domainScreenName},
+ payload: {domainAccountID: domain.accountID, screenName: domainScreenName},
});
}
}
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 568cef73983e8..96700d6b571a3 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1929,6 +1929,9 @@ const config: LinkingOptions['config'] = {
[SCREENS.DOMAIN.SAML]: {
path: ROUTES.DOMAIN_SAML.route,
},
+ [SCREENS.DOMAIN.ADMINS]: {
+ path: ROUTES.DOMAIN_ADMINS.route,
+ },
},
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index dab35a7466d06..edf94161deb50 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1319,10 +1319,10 @@ type SettingsNavigatorParamList = {
subRateID: string;
};
[SCREENS.DOMAIN.VERIFY]: {
- accountID: number;
+ domainAccountID: number;
};
[SCREENS.DOMAIN.VERIFIED]: {
- accountID: number;
+ domainAccountID: number;
};
} & ReimbursementAccountNavigatorParamList;
@@ -2108,10 +2108,10 @@ type MergeTransactionNavigatorParamList = {
type WorkspacesDomainModalNavigatorParamList = {
[SCREENS.WORKSPACES_VERIFY_DOMAIN]: {
- accountID: number;
+ domainAccountID: number;
};
[SCREENS.WORKSPACES_DOMAIN_VERIFIED]: {
- accountID: number;
+ domainAccountID: number;
};
[SCREENS.WORKSPACES_ADD_DOMAIN]: undefined;
[SCREENS.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT]: undefined;
@@ -2119,7 +2119,7 @@ type WorkspacesDomainModalNavigatorParamList = {
accountID: number;
};
[SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED]: {
- accountID: number;
+ domainAccountID: number;
};
};
@@ -2407,10 +2407,13 @@ type WorkspaceSplitNavigatorParamList = {
type DomainSplitNavigatorParamList = {
[SCREENS.DOMAIN.INITIAL]: {
- accountID: number;
+ domainAccountID: number;
};
[SCREENS.DOMAIN.SAML]: {
- accountID: number;
+ domainAccountID: number;
+ };
+ [SCREENS.DOMAIN.ADMINS]: {
+ domainAccountID: number;
};
};
diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx
new file mode 100644
index 0000000000000..f3687dda1c8f4
--- /dev/null
+++ b/src/pages/domain/Admins/DomainAdminsPage.tsx
@@ -0,0 +1,148 @@
+import {adminAccountIDsSelector} from '@selectors/Domain';
+import React from 'react';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import ScreenWrapper from '@components/ScreenWrapper';
+import SearchBar from '@components/SearchBar';
+import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader';
+import SelectionList from '@components/SelectionListWithSections';
+import TableListItem from '@components/SelectionListWithSections/TableListItem';
+import type {ListItem} from '@components/SelectionListWithSections/types';
+import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSearchResults from '@hooks/useSearchResults';
+import useThemeStyles from '@hooks/useThemeStyles';
+import {sortAlphabetically} from '@libs/OptionsListUtils';
+import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
+import tokenizedSearch from '@libs/tokenizedSearch';
+import Navigation from '@navigation/Navigation';
+import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types';
+import type {DomainSplitNavigatorParamList} from '@navigation/types';
+import {getCurrentUserAccountID} from '@userActions/Report';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+
+type DomainAdminsPageProps = PlatformStackScreenProps;
+
+type AdminOption = Omit & {
+ accountID: number;
+ login: string;
+};
+
+function DomainAdminsPage({route}: DomainAdminsPageProps) {
+ const {domainAccountID} = route.params;
+
+ const {translate, formatPhoneNumber, localeCompare} = useLocalize();
+ const styles = useThemeStyles();
+ const illustrations = useMemoizedLazyIllustrations(['Members'] as const);
+ const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const);
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ const [adminAccountIDs, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {
+ canBeMissing: true,
+ selector: adminAccountIDsSelector,
+ });
+ const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true});
+
+ const data: AdminOption[] = [];
+ for (const accountID of adminAccountIDs ?? []) {
+ const details = personalDetails?.[accountID];
+ data.push({
+ keyForList: String(accountID),
+ accountID,
+ login: details?.login ?? '',
+ text: formatPhoneNumber(getDisplayNameOrDefault(details)),
+ alternateText: formatPhoneNumber(details?.login ?? ''),
+ icons: [
+ {
+ source: details?.avatar ?? icons.FallbackAvatar,
+ name: formatPhoneNumber(details?.login ?? ''),
+ type: CONST.ICON_TYPE_AVATAR,
+ id: accountID,
+ },
+ ],
+ });
+ }
+
+ const filterMember = (adminOption: AdminOption, searchQuery: string) => {
+ const results = tokenizedSearch([adminOption], searchQuery, (option) => [option.text ?? '', option.alternateText ?? '']);
+ return results.length > 0;
+ };
+ const sortMembers = (adminOptions: AdminOption[]) => sortAlphabetically(adminOptions, 'text', localeCompare);
+ const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers);
+
+ const getCustomListHeader = () => {
+ if (filteredData.length === 0) {
+ return null;
+ }
+
+ return (
+
+ );
+ };
+
+ const listHeaderContent =
+ data.length > CONST.SEARCH_ITEM_LIMIT ? (
+
+ ) : null;
+
+ if (isLoadingOnyxValue(domainMetadata)) {
+ return ;
+ }
+
+ const currentUserAccountID = getCurrentUserAccountID();
+ const isAdmin = adminAccountIDs?.includes(currentUserAccountID);
+
+ return (
+
+ Navigation.goBack(ROUTES.WORKSPACES_LIST.route)}
+ shouldShow={!isAdmin}
+ shouldForceFullScreen
+ >
+
+ {}}
+ shouldShowListEmptyContent={false}
+ listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]}
+ showScrollIndicator={false}
+ addBottomSafeAreaPadding
+ customListHeader={getCustomListHeader()}
+ />
+
+
+ );
+}
+
+DomainAdminsPage.displayName = 'DomainAdminsPage';
+
+export default DomainAdminsPage;
diff --git a/src/pages/domain/BaseDomainVerifiedPage.tsx b/src/pages/domain/BaseDomainVerifiedPage.tsx
index c88f4c9f7a6c6..90b5c08c3cfff 100644
--- a/src/pages/domain/BaseDomainVerifiedPage.tsx
+++ b/src/pages/domain/BaseDomainVerifiedPage.tsx
@@ -19,18 +19,18 @@ import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
type BaseDomainVerifiedPageProps = {
/** The accountID of the domain */
- accountID: number;
+ domainAccountID: number;
/** Route to redirect to when trying to access the page for an unverified domain */
redirectTo: Route;
};
-function BaseDomainVerifiedPage({accountID, redirectTo}: BaseDomainVerifiedPageProps) {
+function BaseDomainVerifiedPage({domainAccountID, redirectTo}: BaseDomainVerifiedPageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: false});
- const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
+ const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: false});
+ const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainAccountID}`, {canBeMissing: false});
const doesDomainExist = !!domain;
useEffect(() => {
@@ -38,7 +38,7 @@ function BaseDomainVerifiedPage({accountID, redirectTo}: BaseDomainVerifiedPageP
return;
}
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(redirectTo, {forceReplace: true}));
- }, [accountID, domain?.validated, doesDomainExist, redirectTo]);
+ }, [domainAccountID, domain?.validated, doesDomainExist, redirectTo]);
if (isLoadingOnyxValue(domainMetadata, isAdminMetadata)) {
return ;
@@ -65,7 +65,7 @@ function BaseDomainVerifiedPage({accountID, redirectTo}: BaseDomainVerifiedPageP
innerContainerStyle={styles.p10}
buttonText={translate('common.buttonConfirm')}
shouldShowButton
- onButtonPress={() => Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(accountID))}
+ onButtonPress={() => Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(domainAccountID))}
/>
);
diff --git a/src/pages/domain/BaseVerifyDomainPage.tsx b/src/pages/domain/BaseVerifyDomainPage.tsx
index 4a18e68a1f5c9..6f904740af229 100644
--- a/src/pages/domain/BaseVerifyDomainPage.tsx
+++ b/src/pages/domain/BaseVerifyDomainPage.tsx
@@ -38,18 +38,18 @@ function OrderedListRow({index, children}: PropsWithChildren<{index: number}>) {
type BaseVerifyDomainPageProps = {
/** The accountID of the domain */
- accountID: number;
+ domainAccountID: number;
/** Route to navigate to after successful verification */
forwardTo: Route;
};
-function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps) {
+function BaseVerifyDomainPage({domainAccountID, forwardTo}: BaseVerifyDomainPageProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
- const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true});
+ const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true});
const domainName = domain ? Str.extractEmailDomain(domain.email) : '';
const doesDomainExist = !!domain;
@@ -60,21 +60,21 @@ function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps)
return;
}
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(forwardTo, {forceReplace: true}));
- }, [accountID, domain?.hasValidationSucceeded, forwardTo]);
+ }, [domainAccountID, domain?.hasValidationSucceeded, forwardTo]);
useEffect(() => {
if (!doesDomainExist) {
return;
}
- getDomainValidationCode(accountID, domainName);
- }, [accountID, domainName, doesDomainExist]);
+ getDomainValidationCode(domainAccountID, domainName);
+ }, [domainAccountID, domainName, doesDomainExist]);
useEffect(() => {
if (!doesDomainExist) {
return;
}
- resetDomainValidationError(accountID);
- }, [accountID, doesDomainExist]);
+ resetDomainValidationError(domainAccountID);
+ }, [domainAccountID, doesDomainExist]);
if (isLoadingOnyxValue(domainMetadata)) {
return ;
@@ -127,7 +127,7 @@ function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps)
{!!domain.validateCodeError && (
getDomainValidationCode(accountID, domainName)}
+ onRetry={() => getDomainValidationCode(domainAccountID, domainName)}
isButtonSmall
/>
)}
@@ -155,7 +155,7 @@ function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps)
validateDomain(accountID, domainName)}
+ onSubmit={() => validateDomain(domainAccountID, domainName)}
message={getLatestErrorMessage({errors: domain.domainValidationError})}
isAlertVisible={!!domain.domainValidationError}
containerStyles={styles.mb5}
diff --git a/src/pages/domain/DomainAccessRestrictedPage.tsx b/src/pages/domain/DomainAccessRestrictedPage.tsx
index 928c602638e4d..14d2494f7b312 100644
--- a/src/pages/domain/DomainAccessRestrictedPage.tsx
+++ b/src/pages/domain/DomainAccessRestrictedPage.tsx
@@ -41,8 +41,8 @@ function DomainAccessRestrictedPage({route}: DomainAccessRestrictedPageProps) {
const theme = useTheme();
const {translate} = useLocalize();
- const accountID = route.params.accountID;
- const [domainName, domainNameResults] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: false, selector: domainNameSelector});
+ const {domainAccountID} = route.params;
+ const [domainName, domainNameResults] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: false, selector: domainNameSelector});
if (isLoadingOnyxValue(domainNameResults)) {
return ;
@@ -87,7 +87,7 @@ function DomainAccessRestrictedPage({route}: DomainAccessRestrictedPageProps) {
large
success
text={translate('common.verify')}
- onPress={() => Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(accountID))}
+ onPress={() => Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(domainAccountID))}
/>
diff --git a/src/pages/domain/DomainInitialPage.tsx b/src/pages/domain/DomainInitialPage.tsx
index 9bb827fdf219e..00076d3f42948 100644
--- a/src/pages/domain/DomainInitialPage.tsx
+++ b/src/pages/domain/DomainInitialPage.tsx
@@ -54,23 +54,23 @@ function DomainInitialPage({route}: DomainInitialPageProps) {
const {translate} = useLocalize();
const shouldDisplayLHB = !shouldUseNarrowLayout;
- const accountID = route.params?.accountID;
- const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true});
+ const domainAccountID = route.params?.domainAccountID;
+ const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true});
const domainName = domain ? Str.extractEmailDomain(domain.email) : undefined;
- const [isAdmin] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
+ const [isAdmin] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainAccountID}`, {canBeMissing: false});
const domainMenuItems: DomainMenuItem[] = useMemo(() => {
const menuItems: DomainMenuItem[] = [
{
translationKey: 'domain.saml',
icon: icons.UserLock,
- action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.DOMAIN_SAML.getRoute(accountID)))),
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.DOMAIN_SAML.getRoute(domainAccountID)))),
screenName: SCREENS.DOMAIN.SAML,
},
];
return menuItems;
- }, [accountID, singleExecution, waitForNavigate, icons.UserLock]);
+ }, [domainAccountID, singleExecution, waitForNavigate, icons.UserLock]);
useEffect(() => {
if (!domainName) {
diff --git a/src/pages/domain/DomainSamlPage.tsx b/src/pages/domain/DomainSamlPage.tsx
index 52b5c53d1205f..79733f1be4f4a 100644
--- a/src/pages/domain/DomainSamlPage.tsx
+++ b/src/pages/domain/DomainSamlPage.tsx
@@ -35,10 +35,10 @@ function DomainSamlPage({route}: DomainSamlPageProps) {
const {translate} = useLocalize();
const illustrations = useMemoizedLazyIllustrations(['LaptopOnDeskWithCoffeeAndKey', 'LockClosed', 'OpenSafe', 'ShieldYellow'] as const);
- const accountID = route.params.accountID;
- const [domain, domainResults] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true});
- const [isAdmin, isAdminResults] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
- const [domainSettings, domainSettingsResults] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${accountID}`, {
+ const domainAccountID = route.params?.domainAccountID;
+ const [domain, domainResults] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true});
+ const [isAdmin, isAdminResults] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainAccountID}`, {canBeMissing: false});
+ const [domainSettings, domainSettingsResults] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, {
canBeMissing: false,
selector: domainMemberSamlSettingsSelector,
});
@@ -106,7 +106,7 @@ function DomainSamlPage({route}: DomainSamlPageProps) {
childrenStyles={[styles.gap6, styles.pt6]}
>
@@ -143,7 +143,7 @@ function DomainSamlPage({route}: DomainSamlPageProps) {
ctaText={translate('domain.verifyDomain.title')}
ctaAccessibilityLabel={translate('domain.verifyDomain.title')}
onCtaPress={() => {
- Navigation.navigate(ROUTES.DOMAIN_VERIFY.getRoute(accountID));
+ Navigation.navigate(ROUTES.DOMAIN_VERIFY.getRoute(domainAccountID));
}}
illustrationBackgroundColor={colors.blue700}
illustration={illustrations.LaptopOnDeskWithCoffeeAndKey}
diff --git a/src/pages/domain/SamlDomainVerifiedPage.tsx b/src/pages/domain/SamlDomainVerifiedPage.tsx
index 16842dbd284f5..504e2b127a219 100644
--- a/src/pages/domain/SamlDomainVerifiedPage.tsx
+++ b/src/pages/domain/SamlDomainVerifiedPage.tsx
@@ -8,12 +8,12 @@ import BaseDomainVerifiedPage from './BaseDomainVerifiedPage';
type SamlDomainVerifiedPageProps = PlatformStackScreenProps;
function SamlDomainVerifiedPage({route}: SamlDomainVerifiedPageProps) {
- const accountID = route.params.accountID;
+ const {domainAccountID} = route.params;
return (
);
}
diff --git a/src/pages/domain/SamlVerifyDomainPage.tsx b/src/pages/domain/SamlVerifyDomainPage.tsx
index efa027338eb55..98498baba4fbb 100644
--- a/src/pages/domain/SamlVerifyDomainPage.tsx
+++ b/src/pages/domain/SamlVerifyDomainPage.tsx
@@ -8,12 +8,12 @@ import BaseVerifyDomainPage from './BaseVerifyDomainPage';
type SamlVerifyDomainPageProps = PlatformStackScreenProps;
function SamlVerifyDomainPage({route}: SamlVerifyDomainPageProps) {
- const accountID = route.params.accountID;
+ const {domainAccountID} = route.params;
return (
);
}
diff --git a/src/pages/domain/WorkspacesDomainVerifiedPage.tsx b/src/pages/domain/WorkspacesDomainVerifiedPage.tsx
index 94228f7f220de..20ddd797cd69c 100644
--- a/src/pages/domain/WorkspacesDomainVerifiedPage.tsx
+++ b/src/pages/domain/WorkspacesDomainVerifiedPage.tsx
@@ -8,12 +8,12 @@ import BaseDomainVerifiedPage from './BaseDomainVerifiedPage';
type WorkspacesDomainVerifiedPageProps = PlatformStackScreenProps;
function WorkspacesDomainVerifiedPage({route}: WorkspacesDomainVerifiedPageProps) {
- const accountID = route.params.accountID;
+ const {domainAccountID} = route.params;
return (
);
}
diff --git a/src/pages/domain/WorkspacesVerifyDomainPage.tsx b/src/pages/domain/WorkspacesVerifyDomainPage.tsx
index 4c595e92ea124..65e5319e0cc96 100644
--- a/src/pages/domain/WorkspacesVerifyDomainPage.tsx
+++ b/src/pages/domain/WorkspacesVerifyDomainPage.tsx
@@ -8,12 +8,12 @@ import BaseVerifyDomainPage from './BaseVerifyDomainPage';
type WorkspacesVerifyDomainPageProps = PlatformStackScreenProps;
function WorkspacesVerifyDomainPage({route}: WorkspacesVerifyDomainPageProps) {
- const accountID = route.params.accountID;
+ const {domainAccountID} = route.params;
return (
);
}
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index 7677780dec644..6a879ac74b11a 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -502,11 +502,11 @@ function WorkspacesListPage() {
[shouldUseNarrowLayout],
);
- const navigateToDomain = useCallback(({accountID, isAdmin}: {accountID: number; isAdmin: boolean}) => {
+ const navigateToDomain = useCallback(({domainAccountID, isAdmin}: {domainAccountID: number; isAdmin: boolean}) => {
if (!isAdmin) {
- return Navigation.navigate(ROUTES.WORKSPACES_DOMAIN_ACCESS_RESTRICTED.getRoute(accountID));
+ return Navigation.navigate(ROUTES.WORKSPACES_DOMAIN_ACCESS_RESTRICTED.getRoute(domainAccountID));
}
- Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(accountID));
+ Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(domainAccountID));
}, []);
/**
@@ -593,7 +593,7 @@ function WorkspacesListPage() {
listItemType: 'domain',
accountID: domain.accountID,
title: Str.extractEmailDomain(domain.email),
- action: () => navigateToDomain({accountID: domain.accountID, isAdmin}),
+ action: () => navigateToDomain({domainAccountID: domain.accountID, isAdmin}),
isAdmin,
isValidated: domain.validated,
pendingAction: domain.pendingAction,
diff --git a/src/selectors/Domain.ts b/src/selectors/Domain.ts
index af098c732c896..c24e8cc4225c3 100644
--- a/src/selectors/Domain.ts
+++ b/src/selectors/Domain.ts
@@ -1,6 +1,8 @@
import {Str} from 'expensify-common';
import type {OnyxEntry} from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
import type {CardFeeds, Domain, SamlMetadata} from '@src/types/onyx';
+import getEmptyArray from '@src/types/utils/getEmptyArray';
const domainMemberSamlSettingsSelector = (domainSettings: OnyxEntry) => domainSettings?.settings;
@@ -18,4 +20,30 @@ const domainNameSelector = (domain: OnyxEntry) => (domain?.email ? Str.e
const metaIdentitySelector = (samlMetadata: OnyxEntry) => samlMetadata?.metaIdentity;
-export {domainMemberSamlSettingsSelector, domainSamlSettingsStateSelector, domainNameSelector, metaIdentitySelector};
+/**
+ * Extracts a list of admin IDs (accountIDs) from the domain object.
+ * It filters the domain properties for keys starting with the admin permissions prefix
+ * and returns the values as an array of numbers.
+ *
+ * @param domain - The domain object from Onyx
+ * @returns An array of admin account IDs
+ */
+function adminAccountIDsSelector(domain: OnyxEntry): number[] {
+ if (!domain) {
+ return [];
+ }
+
+ return (
+ Object.entries(domain).reduce((acc, [key, value]) => {
+ if (!key.startsWith(ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX) || value === undefined || value === null) {
+ return acc;
+ }
+
+ acc.push(Number(value));
+
+ return acc;
+ }, []) ?? getEmptyArray()
+ );
+}
+
+export {domainMemberSamlSettingsSelector, domainSamlSettingsStateSelector, domainNameSelector, metaIdentitySelector, adminAccountIDsSelector};
diff --git a/src/types/onyx/Domain.ts b/src/types/onyx/Domain.ts
index 400f703a2219d..22a3bd61f6036 100644
--- a/src/types/onyx/Domain.ts
+++ b/src/types/onyx/Domain.ts
@@ -1,5 +1,11 @@
+import type ONYXKEYS from '@src/ONYXKEYS';
import type * as OnyxCommon from './OnyxCommon';
+/**
+ * A utility type that creates a record where all keys are strings that start with a specified prefix.
+ */
+type PrefixedRecord = Record<`${Prefix}${string}`, ValueType>;
+
/** Model of domain data */
type Domain = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Whether the domain is validated */
@@ -40,7 +46,8 @@ type Domain = OnyxCommon.OnyxValueWithOfflineFeedback<{
/** Whether setting SAML required setting has failed and why */
samlRequiredError?: OnyxCommon.Errors;
-}>;
+}> &
+ PrefixedRecord;
/** Model of SAML metadata */
type SamlMetadata = {
diff --git a/tests/unit/DomainSelectorsTest.ts b/tests/unit/DomainSelectorsTest.ts
new file mode 100644
index 0000000000000..3be6dcac32277
--- /dev/null
+++ b/tests/unit/DomainSelectorsTest.ts
@@ -0,0 +1,45 @@
+import {adminAccountIDsSelector} from '@selectors/Domain';
+import type {OnyxEntry} from 'react-native-onyx';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Domain} from '@src/types/onyx';
+
+describe('domainSelectors', () => {
+ describe('adminAccountIDsSelector', () => {
+ it('Should return an empty array if the domain object is undefined', () => {
+ expect(adminAccountIDsSelector(undefined)).toEqual([]);
+ });
+
+ it('Should return an array of admin IDs when keys start with the admin access prefix', () => {
+ const domain = {
+ [`${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}123`]: 321,
+ [`${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}321`]: 123,
+ } as unknown as OnyxEntry;
+
+ expect(adminAccountIDsSelector(domain)).toEqual([321, 123]);
+ });
+
+ it('Should ignore keys that do not start with the admin access prefix', () => {
+ const domain = {
+ [`${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}123`]: 321,
+ somOtherProperty: 'value',
+ } as unknown as OnyxEntry;
+
+ expect(adminAccountIDsSelector(domain)).toEqual([321]);
+ });
+
+ it('Should ignore keys with falsy values even if they have the correct prefix', () => {
+ const domain = {
+ [`${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}123`]: 123,
+ [`${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}0`]: undefined,
+ [`${ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX}999`]: null,
+ } as unknown as OnyxEntry;
+
+ expect(adminAccountIDsSelector(domain)).toEqual([123]);
+ });
+
+ it('Should return an empty array if the domain object is empty', () => {
+ const domain = {} as OnyxEntry;
+ expect(adminAccountIDsSelector(domain)).toEqual([]);
+ });
+ });
+});