From 403585e781b297041fc6ea769fa0097780799bc8 Mon Sep 17 00:00:00 2001 From: war-in Date: Thu, 11 Dec 2025 17:28:07 -0500 Subject: [PATCH 01/21] feat: add DomainAdminsPage.tsx --- .../simple-illustration__approval-members.svg | 10 ++ src/CONST/index.ts | 2 +- src/ONYXKEYS.ts | 4 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../Icon/chunks/illustrations.chunk.ts | 2 + src/languages/en.ts | 4 + src/libs/DomainUtils.ts | 30 ++++ .../Navigators/DomainSplitNavigator.tsx | 7 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/pages/domain/Admins/DomainAdminsPage.tsx | 150 ++++++++++++++++++ src/types/onyx/Domain.ts | 9 +- 13 files changed, 227 insertions(+), 2 deletions(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__approval-members.svg create mode 100644 src/libs/DomainUtils.ts create mode 100644 src/pages/domain/Admins/DomainAdminsPage.tsx 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..f25141b371014 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__approval-members.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 862151c02a737..56549e3eb364c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1852,7 +1852,7 @@ const CONST = { PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, RECHECK_INTERVAL_MS: 60 * 1000, - MAX_REQUEST_RETRIES: 10, + MAX_REQUEST_RETRIES: 1, MAX_OPEN_APP_REQUEST_RETRIES: 2, NETWORK_STATUS: { ONLINE: 'online', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 700a4e3affb4b..82f698af8c69f 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_', + + /** */ + DOMAIN_ADMIN_PERMISSIONS: '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.DOMAIN_ADMIN_PERMISSIONS]: number; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f45c2dee8c61c..2debc94d7156c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3456,6 +3456,10 @@ const ROUTES = { route: 'domain/:accountID/verified', getRoute: (accountID: number) => `domain/${accountID}/verified` as const, }, + DOMAIN_ADMINS: { + route: 'domain/:accountID/admins', + getRoute: (accountID: number) => `domain/${accountID}/admins` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 228174b4b8eb0..4a89c75481cd6 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -852,6 +852,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/languages/en.ts b/src/languages/en.ts index dfad007732621..146642d84696c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7848,6 +7848,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/libs/DomainUtils.ts b/src/libs/DomainUtils.ts new file mode 100644 index 0000000000000..c3b9ce9277ba5 --- /dev/null +++ b/src/libs/DomainUtils.ts @@ -0,0 +1,30 @@ +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import getEmptyArray from '@src/types/utils/getEmptyArray'; + +/** + * 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 selectAdminIDs(domain: OnyxTypes.Domain | undefined): number[] { + if (!domain) { + return []; + } + + return ( + Object.entries(domain) + .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_ADMIN_PERMISSIONS)) + .map(([, value]) => { + const rawValue = typeof value === 'object' && value !== null && 'value' in value ? value.value : value; + return Number(rawValue); + }) + .filter((id) => !Number.isNaN(id)) ?? getEmptyArray() + ); +} + +// eslint-disable-next-line import/prefer-default-export +export {selectAdminIDs}; 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/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 4465babe320cb..953755ce7ffc5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1931,6 +1931,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 19d88a6ab9bb1..8b5538a33b3b1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2412,6 +2412,9 @@ type DomainSplitNavigatorParamList = { [SCREENS.DOMAIN.SAML]: { accountID: number; }; + [SCREENS.DOMAIN.ADMINS]: { + accountID: number; + }; }; type OnboardingModalNavigatorParamList = { diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx new file mode 100644 index 0000000000000..07c1c0695ded8 --- /dev/null +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -0,0 +1,150 @@ +import React, {useCallback} from 'react'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {FallbackAvatar} from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollViewWithContext from '@components/ScrollViewWithContext'; +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 {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 {selectAdminIDs} from '@src/libs/DomainUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type DomainAdminsPageProps = PlatformStackScreenProps; + +type AdminOption = Omit & { + accountID: number; + login: string; +}; + +function DomainAdminsPage({route}: DomainAdminsPageProps) { + const {accountID: domainID} = route.params; + + const {translate, formatPhoneNumber, localeCompare} = useLocalize(); + const styles = useThemeStyles(); + const illustrations = useMemoizedLazyIllustrations(['Members'] as const); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: true}); + const [adminIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, { + canBeMissing: true, + selector: selectAdminIDs, + }); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + + const currentUserAccountID = getCurrentUserAccountID(); + const isAdmin = adminIDs?.includes(currentUserAccountID) ?? false; + + const data: AdminOption[] = []; + for (const accountID of adminIDs ?? []) { + 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 ?? FallbackAvatar, + name: formatPhoneNumber(details?.login ?? ''), + type: CONST.ICON_TYPE_AVATAR, + id: accountID, + }, + ], + }); + } + + const filterMember = useCallback((adminOption: AdminOption, searchQuery: string) => { + const results = tokenizedSearch([adminOption], searchQuery, (option) => [option.text ?? '', option.alternateText ?? '']); + return results.length > 0; + }, []); + const sortMembers = useCallback((adminOptions: AdminOption[]) => sortAlphabetically(adminOptions, 'text', localeCompare), [localeCompare]); + const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); + + const getCustomListHeader = () => { + if (filteredData.length === 0) { + return null; + } + + return ( + + ); + }; + + return ( + + Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} + shouldShow={!domain || !isAdmin} + shouldForceFullScreen + > + + + + CONST.SEARCH_ITEM_LIMIT ? ( + + ) : null + } + listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} + ListItem={TableListItem} + onSelectRow={() => {}} + shouldShowListEmptyContent={false} + listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} + showScrollIndicator={false} + addBottomSafeAreaPadding + customListHeader={getCustomListHeader()} + /> + + + + ); +} + +DomainAdminsPage.displayName = 'DomainAdminsPage'; + +export default DomainAdminsPage; diff --git a/src/types/onyx/Domain.ts b/src/types/onyx/Domain.ts index 400f703a2219d..9fe8551194b9a 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'; +/** + * + */ +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 = { From 09576b3d46f6084e3d73ef5b50d51ced2bcc80ea Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 11:48:35 -0500 Subject: [PATCH 02/21] feat: add FullScreenLoadingIndicator when loading onyx data --- src/pages/domain/Admins/DomainAdminsPage.tsx | 21 +++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 07c1c0695ded8..14169211bcfcb 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,9 +1,10 @@ import React, {useCallback} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollViewWithContext from '@components/ScrollViewWithContext'; +import ScrollView from '@components/ScrollView'; import SearchBar from '@components/SearchBar'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import SelectionList from '@components/SelectionListWithSections'; @@ -21,12 +22,12 @@ 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 {selectAdminIDs} from '@src/libs/DomainUtils'; 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; @@ -43,16 +44,14 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const illustrations = useMemoizedLazyIllustrations(['Members'] as const); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: true}); + const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: true}); + const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainID}`, {canBeMissing: false}); const [adminIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, { canBeMissing: true, selector: selectAdminIDs, }); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const currentUserAccountID = getCurrentUserAccountID(); - const isAdmin = adminIDs?.includes(currentUserAccountID) ?? false; - const data: AdminOption[] = []; for (const accountID of adminIDs ?? []) { const details = personalDetails?.[accountID]; @@ -93,6 +92,10 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { ); }; + if (isLoadingOnyxValue(domainMetadata, isAdminMetadata)) { + return ; + } + return ( Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} - shouldShow={!domain || !isAdmin} + shouldShow={!isLoadingOnyxValue(domainMetadata) && (!domain || !isAdmin)} shouldForceFullScreen > - - + ); From c327e60739d44f75a905d01b84ec5358f35a544a Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 11:53:07 -0500 Subject: [PATCH 03/21] chore: remove outdated isLoadingOnyxValue check --- src/pages/domain/Admins/DomainAdminsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 14169211bcfcb..c490b89db6906 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -105,7 +105,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { > Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} - shouldShow={!isLoadingOnyxValue(domainMetadata) && (!domain || !isAdmin)} + shouldShow={!domain || !isAdmin} shouldForceFullScreen > Date: Fri, 12 Dec 2025 12:14:32 -0500 Subject: [PATCH 04/21] fix: revert MAX_REQUEST_RETRIES change --- src/CONST/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 2435c408ec966..975cbd0052a77 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1852,7 +1852,7 @@ const CONST = { PROCESS_REQUEST_DELAY_MS: 1000, MAX_PENDING_TIME_MS: 10 * 1000, RECHECK_INTERVAL_MS: 60 * 1000, - MAX_REQUEST_RETRIES: 1, + MAX_REQUEST_RETRIES: 10, MAX_OPEN_APP_REQUEST_RETRIES: 2, NETWORK_STATUS: { ONLINE: 'online', From 3f8acf74adb7aa0748c9663b8a23f7387d222f6e Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 12:14:49 -0500 Subject: [PATCH 05/21] chore: add descriptions --- src/ONYXKEYS.ts | 2 +- src/types/onyx/Domain.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 82f698af8c69f..82fbe61872670 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -732,7 +732,7 @@ const ONYXKEYS = { /** SAML login metadata for a domain */ SAML_METADATA: 'saml_metadata_', - /** */ + /** Stores domain admin account ID */ DOMAIN_ADMIN_PERMISSIONS: 'expensify_adminPermissions_', }, diff --git a/src/types/onyx/Domain.ts b/src/types/onyx/Domain.ts index 9fe8551194b9a..73f1e7c1adde0 100644 --- a/src/types/onyx/Domain.ts +++ b/src/types/onyx/Domain.ts @@ -2,7 +2,7 @@ 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>; From b0fbb7c5b523c14b97e723c6cb74707da5a7b7ca Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 12:29:10 -0500 Subject: [PATCH 06/21] fix: use useMemoizedLazyExpensifyIcons hook --- src/pages/domain/Admins/DomainAdminsPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index c490b89db6906..22558fbfe0d7b 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -2,7 +2,6 @@ import React, {useCallback} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import SearchBar from '@components/SearchBar'; @@ -10,7 +9,7 @@ import CustomListHeader from '@components/SelectionListWithModal/CustomListHeade import SelectionList from '@components/SelectionListWithSections'; import TableListItem from '@components/SelectionListWithSections/TableListItem'; import type {ListItem} from '@components/SelectionListWithSections/types'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; @@ -42,6 +41,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['Members'] as const); + const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const); const {shouldUseNarrowLayout} = useResponsiveLayout(); const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: true}); @@ -63,7 +63,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { alternateText: formatPhoneNumber(details?.login ?? ''), icons: [ { - source: details?.avatar ?? FallbackAvatar, + source: details?.avatar ?? icons.FallbackAvatar, name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, From 44167bfc3b62706cfe573f660d9bb76059b821cf Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 12:42:16 -0500 Subject: [PATCH 07/21] refactor: update the selectAdminIDs --- src/libs/DomainUtils.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/libs/DomainUtils.ts b/src/libs/DomainUtils.ts index c3b9ce9277ba5..bdea6a9efc709 100644 --- a/src/libs/DomainUtils.ts +++ b/src/libs/DomainUtils.ts @@ -16,13 +16,15 @@ function selectAdminIDs(domain: OnyxTypes.Domain | undefined): number[] { } return ( - Object.entries(domain) - .filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_ADMIN_PERMISSIONS)) - .map(([, value]) => { - const rawValue = typeof value === 'object' && value !== null && 'value' in value ? value.value : value; - return Number(rawValue); - }) - .filter((id) => !Number.isNaN(id)) ?? getEmptyArray() + Object.entries(domain).reduce((acc, [key, value]) => { + if (!key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_ADMIN_PERMISSIONS) && !value) { + return acc; + } + + acc.push(Number(value)); + + return acc; + }, []) ?? getEmptyArray() ); } From c74bc44f2df84ca88888e5d73e7ab2046abbe648 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 12:53:28 -0500 Subject: [PATCH 08/21] fix: memoize admins --- src/pages/domain/Admins/DomainAdminsPage.tsx | 48 ++++++++++++-------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 22558fbfe0d7b..6a96ff6f3570c 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -52,25 +52,33 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { }); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const data: AdminOption[] = []; - for (const accountID of adminIDs ?? []) { - 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 data: AdminOption[] = useMemo(() => { + if (!adminIDs) { + return []; + } + + const result = []; + for (const accountID of adminIDs) { + const details = personalDetails?.[accountID]; + result.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, + }, + ], + }); + } + + return result; + }, [adminIDs, formatPhoneNumber, icons.FallbackAvatar, personalDetails]); const filterMember = useCallback((adminOption: AdminOption, searchQuery: string) => { const results = tokenizedSearch([adminOption], searchQuery, (option) => [option.text ?? '', option.alternateText ?? '']); From 51722150e172fa9512c0f609847037c443578852 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 12:58:15 -0500 Subject: [PATCH 09/21] refactor: make DomainAdminsPage.tsx react compiler compatible --- src/pages/domain/Admins/DomainAdminsPage.tsx | 54 +++++++++----------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 6a96ff6f3570c..f622c853f83e0 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useMemo} from 'react'; +import React from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -52,39 +52,31 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { }); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const data: AdminOption[] = useMemo(() => { - if (!adminIDs) { - return []; - } - - const result = []; - for (const accountID of adminIDs) { - const details = personalDetails?.[accountID]; - result.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, - }, - ], - }); - } - - return result; - }, [adminIDs, formatPhoneNumber, icons.FallbackAvatar, personalDetails]); + const data: AdminOption[] = [] + for (const accountID of adminIDs ?? []) { + 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 = useCallback((adminOption: AdminOption, searchQuery: string) => { + const filterMember = (adminOption: AdminOption, searchQuery: string) => { const results = tokenizedSearch([adminOption], searchQuery, (option) => [option.text ?? '', option.alternateText ?? '']); return results.length > 0; - }, []); - const sortMembers = useCallback((adminOptions: AdminOption[]) => sortAlphabetically(adminOptions, 'text', localeCompare), [localeCompare]); + }; + const sortMembers = (adminOptions: AdminOption[]) => sortAlphabetically(adminOptions, 'text', localeCompare); const [inputValue, setInputValue, filteredData] = useSearchResults(data, filterMember, sortMembers); const getCustomListHeader = () => { From 6bf281806c7c64da1af4dacdc8376a3306216958 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 13:01:05 -0500 Subject: [PATCH 10/21] chore: compress svg --- .../simple-illustration__approval-members.svg | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/assets/images/simple-illustrations/simple-illustration__approval-members.svg b/assets/images/simple-illustrations/simple-illustration__approval-members.svg index f25141b371014..5e0837a3e14fc 100644 --- a/assets/images/simple-illustrations/simple-illustration__approval-members.svg +++ b/assets/images/simple-illustrations/simple-illustration__approval-members.svg @@ -1,10 +1 @@ - - - - - - - - - - + \ No newline at end of file From 0c01f9c5673df2f3ad815a00ae57c1e81c09f8da Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 13:02:20 -0500 Subject: [PATCH 11/21] chore: fix prettier --- src/pages/domain/Admins/DomainAdminsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index f622c853f83e0..f74cfcc1db772 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -52,7 +52,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { }); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); - const data: AdminOption[] = [] + const data: AdminOption[] = []; for (const accountID of adminIDs ?? []) { const details = personalDetails?.[accountID]; data.push({ From ad5a8c097f3b757a0b4cc833fb62f3fff1b1f4c1 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 13:21:38 -0500 Subject: [PATCH 12/21] refactor: rename accountID to domainAccountID --- src/ROUTES.ts | 20 ++++++++++---------- src/libs/Navigation/types.ts | 16 ++++++++-------- src/pages/domain/Admins/DomainAdminsPage.tsx | 10 +++++----- src/pages/domain/DomainInitialPage.tsx | 10 +++++----- src/pages/domain/DomainSamlPage.tsx | 14 +++++++------- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 8217bc3a4aa80..7e38a6bec1e99 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3439,24 +3439,24 @@ const ROUTES = { getRoute: (accountID: number) => `workspaces/domain-access-restricted/${accountID}` 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/:accountID/admins', - getRoute: (accountID: number) => `domain/${accountID}/admins` as const, + route: 'domain/:domainAccountID/admins', + getRoute: (domainAccountID: number) => `domain/${domainAccountID}/admins` as const, }, } as const; diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index d7ff3c4031e84..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,13 +2407,13 @@ type WorkspaceSplitNavigatorParamList = { type DomainSplitNavigatorParamList = { [SCREENS.DOMAIN.INITIAL]: { - accountID: number; + domainAccountID: number; }; [SCREENS.DOMAIN.SAML]: { - accountID: number; + domainAccountID: number; }; [SCREENS.DOMAIN.ADMINS]: { - accountID: number; + domainAccountID: number; }; }; diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index f74cfcc1db772..74500ff468415 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -28,7 +28,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; -type DomainAdminsPageProps = PlatformStackScreenProps; +type DomainAdminsPageProps = PlatformStackScreenProps; type AdminOption = Omit & { accountID: number; @@ -36,7 +36,7 @@ type AdminOption = Omit & { }; function DomainAdminsPage({route}: DomainAdminsPageProps) { - const {accountID: domainID} = route.params; + const {domainAccountID} = route.params; const {translate, formatPhoneNumber, localeCompare} = useLocalize(); const styles = useThemeStyles(); @@ -44,9 +44,9 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, {canBeMissing: true}); - const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainID}`, {canBeMissing: false}); - const [adminIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, { + const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true}); + const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainAccountID}`, {canBeMissing: false}); + const [adminIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: selectAdminIDs, }); diff --git a/src/pages/domain/DomainInitialPage.tsx b/src/pages/domain/DomainInitialPage.tsx index 9bb827fdf219e..cfc49ee667780 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; + 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..f47b5dc90c560 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; + 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} From 10fdb0c5d307a1a876617045edcf2cb172a458d1 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 14:30:50 -0500 Subject: [PATCH 13/21] chore: add translations --- src/languages/de.ts | 1 + src/languages/es.ts | 4 ++++ src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 9 files changed, 12 insertions(+) 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/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, From cfd9c30e440bf1777af51227de7957af177fdef5 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 14:32:15 -0500 Subject: [PATCH 14/21] chore: rename the rest of accountIDs to domainAccountID --- src/pages/domain/DomainAccessRestrictedPage.tsx | 6 +++--- src/pages/domain/SamlDomainVerifiedPage.tsx | 6 +++--- src/pages/domain/SamlVerifyDomainPage.tsx | 6 +++--- src/pages/domain/WorkspacesDomainVerifiedPage.tsx | 6 +++--- src/pages/domain/WorkspacesVerifyDomainPage.tsx | 6 +++--- 5 files changed, 15 insertions(+), 15 deletions(-) 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/SamlDomainVerifiedPage.tsx b/src/pages/domain/SamlDomainVerifiedPage.tsx index 16842dbd284f5..0c58b3dfcd9c5 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..1763c94ebe44a 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..419007fa92857 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..234ff194a3bea 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 ( ); } From a17f299a77e85e21d59d89683046dc703729c0c1 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 14:33:27 -0500 Subject: [PATCH 15/21] refactor: apply review comments --- src/ONYXKEYS.ts | 4 +-- src/libs/DomainUtils.ts | 32 -------------------- src/pages/domain/Admins/DomainAdminsPage.tsx | 18 ++++++----- src/selectors/Domain.ts | 30 +++++++++++++++++- src/types/onyx/Domain.ts | 2 +- 5 files changed, 42 insertions(+), 44 deletions(-) delete mode 100644 src/libs/DomainUtils.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 82fbe61872670..2ad248fa1c63d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -733,7 +733,7 @@ const ONYXKEYS = { SAML_METADATA: 'saml_metadata_', /** Stores domain admin account ID */ - DOMAIN_ADMIN_PERMISSIONS: 'expensify_adminPermissions_', + EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', }, /** List of Form ids */ @@ -1120,7 +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.DOMAIN_ADMIN_PERMISSIONS]: number; + [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number; }; type OnyxValuesMapping = { diff --git a/src/libs/DomainUtils.ts b/src/libs/DomainUtils.ts deleted file mode 100644 index bdea6a9efc709..0000000000000 --- a/src/libs/DomainUtils.ts +++ /dev/null @@ -1,32 +0,0 @@ -import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; -import getEmptyArray from '@src/types/utils/getEmptyArray'; - -/** - * 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 selectAdminIDs(domain: OnyxTypes.Domain | undefined): number[] { - if (!domain) { - return []; - } - - return ( - Object.entries(domain).reduce((acc, [key, value]) => { - if (!key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_ADMIN_PERMISSIONS) && !value) { - return acc; - } - - acc.push(Number(value)); - - return acc; - }, []) ?? getEmptyArray() - ); -} - -// eslint-disable-next-line import/prefer-default-export -export {selectAdminIDs}; diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 74500ff468415..283d24e0f0574 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,3 +1,4 @@ +import {adminAccountIDsSelector} from '@selectors/Domain'; import React from 'react'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; @@ -21,8 +22,8 @@ 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 {selectAdminIDs} from '@src/libs/DomainUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -44,16 +45,14 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar'] as const); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true}); - const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${domainAccountID}`, {canBeMissing: false}); - const [adminIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { + const [adminAccountIDs, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, - selector: selectAdminIDs, + selector: adminAccountIDsSelector, }); const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); const data: AdminOption[] = []; - for (const accountID of adminIDs ?? []) { + for (const accountID of adminAccountIDs ?? []) { const details = personalDetails?.[accountID]; data.push({ keyForList: String(accountID), @@ -92,10 +91,13 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { ); }; - if (isLoadingOnyxValue(domainMetadata, isAdminMetadata)) { + if (isLoadingOnyxValue(domainMetadata)) { return ; } + const currentUserAccountID = getCurrentUserAccountID(); + const isAdmin = adminAccountIDs?.includes(currentUserAccountID); + return ( Navigation.goBack(ROUTES.WORKSPACES_LIST.route)} - shouldShow={!domain || !isAdmin} + shouldShow={!isAdmin} shouldForceFullScreen > ) => 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) { + 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 73f1e7c1adde0..22a3bd61f6036 100644 --- a/src/types/onyx/Domain.ts +++ b/src/types/onyx/Domain.ts @@ -47,7 +47,7 @@ type Domain = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether setting SAML required setting has failed and why */ samlRequiredError?: OnyxCommon.Errors; }> & - PrefixedRecord; + PrefixedRecord; /** Model of SAML metadata */ type SamlMetadata = { From ff1edc3606ef2b4f65a3540070e497cf9b7fcace Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 14:58:19 -0500 Subject: [PATCH 16/21] chore: add tests for selector --- src/selectors/Domain.ts | 2 +- tests/unit/DomainSelectorsTest.ts | 45 +++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/unit/DomainSelectorsTest.ts diff --git a/src/selectors/Domain.ts b/src/selectors/Domain.ts index 9b2b7df576e48..c24e8cc4225c3 100644 --- a/src/selectors/Domain.ts +++ b/src/selectors/Domain.ts @@ -35,7 +35,7 @@ function adminAccountIDsSelector(domain: OnyxEntry): number[] { return ( Object.entries(domain).reduce((acc, [key, value]) => { - if (!key.startsWith(ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX) && !value) { + if (!key.startsWith(ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX) || value === undefined || value === null) { return acc; } 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([]); + }); + }); +}); From faed9df4c33e0a490b09a6e7863ca24731ebcb6e Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 15:48:55 -0500 Subject: [PATCH 17/21] chore: fix lint --- src/components/Navigation/NavigationTabBar/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) => { From c52e28742418c01eadde2835bc42a5c8cabba43b Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 15:53:04 -0500 Subject: [PATCH 18/21] refactor: move listHeaderContent up --- src/pages/domain/Admins/DomainAdminsPage.tsx | 21 ++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 283d24e0f0574..89618bd85500d 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -91,6 +91,16 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { ); }; + const listHeaderContent = + data.length > CONST.SEARCH_ITEM_LIMIT ? ( + + ) : null; + if (isLoadingOnyxValue(domainMetadata)) { return ; } @@ -125,16 +135,7 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { CONST.SEARCH_ITEM_LIMIT ? ( - - ) : null - } + listHeaderContent={listHeaderContent} listHeaderWrapperStyle={[styles.ph9, styles.pv3, styles.pb5]} ListItem={TableListItem} onSelectRow={() => {}} From 9ffec3a92b77410c2cb2be08c7df2ddb41c8e016 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 17:09:28 -0500 Subject: [PATCH 19/21] refactor: migrate the rest of accountIDs --- src/ROUTES.ts | 16 +++++++-------- .../GetStateForActionHandlers.ts | 2 +- .../createRootStackNavigator/types.ts | 2 +- .../usePreloadFullScreenNavigators.ts | 2 +- .../helpers/navigateToWorkspacesPage.ts | 2 +- src/pages/domain/BaseDomainVerifiedPage.tsx | 12 +++++------ src/pages/domain/BaseVerifyDomainPage.tsx | 20 +++++++++---------- src/pages/domain/DomainInitialPage.tsx | 2 +- src/pages/domain/SamlDomainVerifiedPage.tsx | 2 +- src/pages/domain/SamlVerifyDomainPage.tsx | 2 +- .../domain/WorkspacesDomainVerifiedPage.tsx | 2 +- .../domain/WorkspacesVerifyDomainPage.tsx | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 8 ++++---- 13 files changed, 37 insertions(+), 37 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 7e38a6bec1e99..f56f6a9d7815a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3421,22 +3421,22 @@ 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/:domainAccountID', 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/AppNavigator/usePreloadFullScreenNavigators.ts b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts index 264d8d6e2219f..0e0260b882953 100644 --- a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts +++ b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts @@ -25,7 +25,7 @@ import {getPreservedNavigatorState} from './createSplitNavigator/usePreserveNavi const TIMING_TO_CALL_PRELOAD = 1000; // Currently, only the Workspaces, Account tabs are preloaded. The remaining tabs will be supported soon. -const TABS_TO_PRELOAD = [NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS]; +const TABS_TO_PRELOAD = [NAVIGATION_TABS.SETTINGS]; function preloadWorkspacesTab(navigation: PlatformStackNavigationProp) { const state = getWorkspacesTabStateFromSessionStorage() ?? navigation.getState(); 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/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/DomainInitialPage.tsx b/src/pages/domain/DomainInitialPage.tsx index cfc49ee667780..00076d3f42948 100644 --- a/src/pages/domain/DomainInitialPage.tsx +++ b/src/pages/domain/DomainInitialPage.tsx @@ -54,7 +54,7 @@ function DomainInitialPage({route}: DomainInitialPageProps) { const {translate} = useLocalize(); const shouldDisplayLHB = !shouldUseNarrowLayout; - const {domainAccountID} = route.params; + 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}${domainAccountID}`, {canBeMissing: false}); diff --git a/src/pages/domain/SamlDomainVerifiedPage.tsx b/src/pages/domain/SamlDomainVerifiedPage.tsx index 0c58b3dfcd9c5..504e2b127a219 100644 --- a/src/pages/domain/SamlDomainVerifiedPage.tsx +++ b/src/pages/domain/SamlDomainVerifiedPage.tsx @@ -12,7 +12,7 @@ function SamlDomainVerifiedPage({route}: SamlDomainVerifiedPageProps) { return ( ); diff --git a/src/pages/domain/SamlVerifyDomainPage.tsx b/src/pages/domain/SamlVerifyDomainPage.tsx index 1763c94ebe44a..98498baba4fbb 100644 --- a/src/pages/domain/SamlVerifyDomainPage.tsx +++ b/src/pages/domain/SamlVerifyDomainPage.tsx @@ -12,7 +12,7 @@ function SamlVerifyDomainPage({route}: SamlVerifyDomainPageProps) { return ( ); diff --git a/src/pages/domain/WorkspacesDomainVerifiedPage.tsx b/src/pages/domain/WorkspacesDomainVerifiedPage.tsx index 419007fa92857..20ddd797cd69c 100644 --- a/src/pages/domain/WorkspacesDomainVerifiedPage.tsx +++ b/src/pages/domain/WorkspacesDomainVerifiedPage.tsx @@ -12,7 +12,7 @@ function WorkspacesDomainVerifiedPage({route}: WorkspacesDomainVerifiedPageProps return ( ); diff --git a/src/pages/domain/WorkspacesVerifyDomainPage.tsx b/src/pages/domain/WorkspacesVerifyDomainPage.tsx index 234ff194a3bea..65e5319e0cc96 100644 --- a/src/pages/domain/WorkspacesVerifyDomainPage.tsx +++ b/src/pages/domain/WorkspacesVerifyDomainPage.tsx @@ -12,7 +12,7 @@ function WorkspacesVerifyDomainPage({route}: WorkspacesVerifyDomainPageProps) { 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, From a34c447426eca1a1bd4285c66b12b91df35f4695 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 17:19:15 -0500 Subject: [PATCH 20/21] refactor: cleanup --- .../usePreloadFullScreenNavigators.ts | 2 +- src/pages/domain/Admins/DomainAdminsPage.tsx | 33 ++++++++----------- src/pages/domain/DomainSamlPage.tsx | 2 +- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts index 0e0260b882953..264d8d6e2219f 100644 --- a/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts +++ b/src/libs/Navigation/AppNavigator/usePreloadFullScreenNavigators.ts @@ -25,7 +25,7 @@ import {getPreservedNavigatorState} from './createSplitNavigator/usePreserveNavi const TIMING_TO_CALL_PRELOAD = 1000; // Currently, only the Workspaces, Account tabs are preloaded. The remaining tabs will be supported soon. -const TABS_TO_PRELOAD = [NAVIGATION_TABS.SETTINGS]; +const TABS_TO_PRELOAD = [NAVIGATION_TABS.WORKSPACES, NAVIGATION_TABS.SETTINGS]; function preloadWorkspacesTab(navigation: PlatformStackNavigationProp) { const state = getWorkspacesTabStateFromSessionStorage() ?? navigation.getState(); diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 89618bd85500d..07b9bbd4443b7 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -4,7 +4,6 @@ import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; import SearchBar from '@components/SearchBar'; import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; import SelectionList from '@components/SelectionListWithSections'; @@ -126,26 +125,20 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { icon={illustrations.Members} shouldShowBackButton={shouldUseNarrowLayout} /> - - {}} + shouldShowListEmptyContent={false} + listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} + showScrollIndicator={false} addBottomSafeAreaPadding - style={[styles.settingsPageBackground, styles.flex1, styles.w100]} - > - {}} - shouldShowListEmptyContent={false} - listItemTitleContainerStyles={shouldUseNarrowLayout ? undefined : [styles.pr3]} - showScrollIndicator={false} - addBottomSafeAreaPadding - customListHeader={getCustomListHeader()} - /> - + customListHeader={getCustomListHeader()} + /> + ` ); diff --git a/src/pages/domain/DomainSamlPage.tsx b/src/pages/domain/DomainSamlPage.tsx index f47b5dc90c560..79733f1be4f4a 100644 --- a/src/pages/domain/DomainSamlPage.tsx +++ b/src/pages/domain/DomainSamlPage.tsx @@ -35,7 +35,7 @@ function DomainSamlPage({route}: DomainSamlPageProps) { const {translate} = useLocalize(); const illustrations = useMemoizedLazyIllustrations(['LaptopOnDeskWithCoffeeAndKey', 'LockClosed', 'OpenSafe', 'ShieldYellow'] as const); - const {domainAccountID} = route.params; + 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}`, { From b8cc828259f2b42969651b476d25ecae48861677 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 17:24:22 -0500 Subject: [PATCH 21/21] chore: remove the mistyped character --- src/pages/domain/Admins/DomainAdminsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 07b9bbd4443b7..f3687dda1c8f4 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -138,7 +138,6 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { addBottomSafeAreaPadding customListHeader={getCustomListHeader()} /> - ` );