diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 91b60f5acff8..486f5b6a5d0f 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -761,6 +761,9 @@ const ONYXKEYS = { /** Stores domain admin account ID */ EXPENSIFY_ADMIN_ACCESS_PREFIX: 'expensify_adminPermissions_', + /** Stores domain security group */ + DOMAIN_SECURITY_GROUP_PREFIX: 'expensify_securityGroup_', + /** Pending actions for a domain */ DOMAIN_PENDING_ACTIONS: 'domainPendingActions_', @@ -1160,6 +1163,7 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.EXPENSIFY_ADMIN_ACCESS_PREFIX]: number; [ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions; [ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors; + [ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP_PREFIX]: OnyxTypes.DomainSecurityGroup; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d3624fc2eea5..5b6616ddf753 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3575,6 +3575,18 @@ const ROUTES = { route: 'domain/:domainAccountID/admins/invite', getRoute: (domainAccountID: number) => `domain/${domainAccountID}/admins/invite` as const, }, + DOMAIN_MEMBERS: { + route: 'domain/:domainAccountID/members', + getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members` as const, + }, + DOMAIN_MEMBER_DETAILS: { + route: 'domain/:domainAccountID/members/:accountID', + getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}` as const, + }, + DOMAIN_LOCK_ACCOUNT: { + route: 'domain/:domainAccountID/members/:accountID/lock-account', + getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}/lock-account` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a7eab1b46f70..53c82f57b498 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -872,6 +872,9 @@ const SCREENS = { ADMINS_SETTINGS: 'Admins_Settings', ADD_PRIMARY_CONTACT: 'Add_Primary_Contact', ADD_ADMIN: 'Domain_Add_Admin', + MEMBERS: 'Domain_Members', + MEMBER_DETAILS: 'Member_Details', + MEMBER_LOCK_ACCOUNT: 'Member_Lock_Account', }, } as const; diff --git a/src/languages/en.ts b/src/languages/en.ts index fc3b9e4cbd34..bace43c0093f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7888,6 +7888,15 @@ const translations = { invite: 'Invite', addAdminError: 'Unable to add this member as an admin. Please try again.', }, + members: { + title: 'Members', + findMember: 'Find member', + closeAccount: 'Close account', + reportSuspiciousActivityPrompt: (email: string) => + `Are you sure? This will lock ${email}'s account.

Our team will then review the account and remove any unauthorized access. To regain access, they'll need to work with Concierge.`, + reportSuspiciousActivityConfirmationTitle: 'We’ve received your request', + reportSuspiciousActivityConfirmationPrompt: 'We’ll review the account to verify it’s safe to unlock and reach out via Concierge with any questions.', + }, }, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 77ba229ed0c6..a00e8b0a83e0 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -800,6 +800,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/domain/Admins/DomainAdminsSettingsPage').default, [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: () => require('../../../../pages/domain/Admins/DomainAddPrimaryContactPage').default, [SCREENS.DOMAIN.ADD_ADMIN]: () => require('../../../../pages/domain/Admins/DomainAddAdminPage').default, + [SCREENS.DOMAIN.MEMBER_DETAILS]: () => require('../../../../pages/domain/Members/MembersDetailsPage').default, + [SCREENS.DOMAIN.MEMBER_LOCK_ACCOUNT]: () => require('../../../../pages/domain/Members/DomainReportSuspiciousActivityPage').default, }); const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx index 1c94244e75d7..649f911492e0 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx @@ -14,6 +14,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 loadDomainMembersPage = () => require('../../../../pages/domain/Members/DomainMembersPage').default; const Split = createSplitNavigator(); @@ -50,6 +51,12 @@ function DomainSplitNavigator({route, navigation}: PlatformStackScreenProps + + diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts index 3a1e3bad6016..24970b74f655 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts @@ -6,6 +6,7 @@ const DOMAIN_TO_RHP: Partial['config'] = { [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: { path: ROUTES.DOMAIN_ADD_PRIMARY_CONTACT.route, }, + [SCREENS.DOMAIN.MEMBER_DETAILS]: { + path: ROUTES.DOMAIN_MEMBER_DETAILS.route, + }, + [SCREENS.DOMAIN.MEMBER_LOCK_ACCOUNT]: { + path: ROUTES.DOMAIN_LOCK_ACCOUNT.route, + }, }, }, [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: { @@ -1999,6 +2005,9 @@ const config: LinkingOptions['config'] = { [SCREENS.DOMAIN.ADMINS]: { path: ROUTES.DOMAIN_ADMINS.route, }, + [SCREENS.DOMAIN.MEMBERS]: { + path: ROUTES.DOMAIN_MEMBERS.route, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f157f6f8ada3..0e03324cb313 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1384,6 +1384,14 @@ type SettingsNavigatorParamList = { [SCREENS.DOMAIN.ADD_ADMIN]: { domainAccountID: number; }; + [SCREENS.DOMAIN.MEMBER_DETAILS]: { + domainAccountID: number; + accountID: number; + }; + [SCREENS.DOMAIN.MEMBER_LOCK_ACCOUNT]: { + domainAccountID: number; + accountID: number; + }; } & ReimbursementAccountNavigatorParamList; type DomainCardNavigatorParamList = { @@ -2487,6 +2495,13 @@ type DomainSplitNavigatorParamList = { [SCREENS.DOMAIN.ADMINS]: { domainAccountID: number; }; + [SCREENS.DOMAIN.MEMBERS]: { + domainAccountID: number; + }; + [SCREENS.DOMAIN.MEMBER_DETAILS]: { + domainAccountID: number; + accountID: number; + }; }; type OnboardingModalNavigatorParamList = { diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 140fc942f576..e85a19b652a5 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -1406,7 +1406,7 @@ function setIsDebugModeEnabled(isDebugModeEnabled: boolean) { Onyx.set(ONYXKEYS.IS_DEBUG_MODE_ENABLED, isDebugModeEnabled); } -function lockAccount() { +function lockAccount(accountID?: number) { const optimisticData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -1445,7 +1445,7 @@ function lockAccount() { ]; const params: LockAccountParams = { - accountID: currentUserAccountID, + accountID: accountID ?? currentUserAccountID, }; // We need to know if this command fails so that we can navigate the user to a failure page. diff --git a/src/pages/domain/Admins/DomainAdminDetailsPage.tsx b/src/pages/domain/Admins/DomainAdminDetailsPage.tsx index e7007125178e..80ec705c2dc2 100644 --- a/src/pages/domain/Admins/DomainAdminDetailsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminDetailsPage.tsx @@ -1,100 +1,40 @@ -import {Str} from 'expensify-common'; -import React from 'react'; -import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import Avatar from '@components/Avatar'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItem from '@components/MenuItem'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import ScrollView from '@components/ScrollView'; -import Text from '@components/Text'; +import React, {useMemo} from 'react'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {getDisplayNameOrDefault, getPhoneNumber} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@navigation/types'; -import DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; +import BaseDomainMemberDetailsComponent from '@pages/domain/BaseDomainMemberDetailsComponent'; +import type {MemberDetailsMenuItem} from '@pages/domain/BaseDomainMemberDetailsComponent'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {PersonalDetailsList} from '@src/types/onyx'; type DomainAdminDetailsPageProps = PlatformStackScreenProps; function DomainAdminDetailsPage({route}: DomainAdminDetailsPageProps) { const {domainAccountID, accountID} = route.params; - const styles = useThemeStyles(); - const {translate, formatPhoneNumber} = useLocalize(); + const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Info'] as const); - // eslint-disable-next-line rulesdir/no-inline-useOnyx-selector - const [adminPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, { - canBeMissing: true, - selector: (personalDetailsList: OnyxEntry) => personalDetailsList?.[accountID], - }); - - const displayName = formatPhoneNumber(getDisplayNameOrDefault(adminPersonalDetails)); - const memberLogin = adminPersonalDetails?.login ?? ''; - const isSMSLogin = Str.isSMSLogin(memberLogin); - const phoneNumber = getPhoneNumber(adminPersonalDetails); - const fallbackIcon = adminPersonalDetails?.fallbackIcon ?? ''; + const menuItems = useMemo( + (): MemberDetailsMenuItem[] => [ + { + key: 'profile', + title: translate('common.profile'), + icon: icons.Info, + onPress: () => Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute())), + shouldShowRightIcon: true, + }, + ], + [accountID, icons.Info, translate], + ); return ( - - - - - - - - - - {!!displayName && ( - - {displayName} - - )} - - - - Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute()))} - shouldShowRightIcon - /> - - - - - + ); } diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 3f9340e64608..355b9228db0c 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,34 +1,17 @@ import {adminAccountIDsSelector, technicalContactSettingsSelector} from '@selectors/Domain'; -import React from 'react'; -import {View} from 'react-native'; +import React, {useCallback} from 'react'; import Badge from '@components/Badge'; import Button from '@components/Button'; -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'; -// eslint-disable-next-line no-restricted-imports -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 {getLatestError} from '@libs/ErrorUtils'; -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 DomainNotFoundPageWrapper from '@pages/domain/DomainNotFoundPageWrapper'; -import {clearAddAdminError} from '@userActions/Domain'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; 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'; @@ -36,15 +19,9 @@ 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 {translate} = useLocalize(); const styles = useThemeStyles(); const illustrations = useMemoizedLazyIllustrations(['Members']); const icons = useMemoizedLazyExpensifyIcons(['FallbackAvatar', 'Gear', 'Plus']); @@ -54,139 +31,50 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { canBeMissing: true, selector: adminAccountIDsSelector, }); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + const [technicalContactSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, { canBeMissing: false, selector: technicalContactSettingsSelector, }); - const [domainErrors] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, {canBeMissing: true}); - const [domainPendingActions] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, {canBeMissing: true}); const currentUserAccountID = getCurrentUserAccountID(); const isAdmin = adminAccountIDs?.includes(currentUserAccountID); - const data: AdminOption[] = []; - for (const accountID of adminAccountIDs ?? []) { - const details = personalDetails?.[accountID]; - const isPrimaryContact = technicalContactSettings?.technicalContactEmail === details?.login; - 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, - }, - ], - rightElement: isPrimaryContact && , - errors: getLatestError(domainErrors?.adminErrors?.[accountID]?.errors), - pendingAction: domainPendingActions?.admin?.[accountID]?.pendingAction, - }); - } - - 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; + const getCustomRightElement = useCallback( + (accountID: number) => { + const login = personalDetails?.[accountID]?.login; + if (technicalContactSettings?.technicalContactEmail !== login) { + return null; } + return ; + }, + [personalDetails, technicalContactSettings?.technicalContactEmail, translate], + ); - return ( - - ); - }; - - const getHeaderButtons = () => { - if (!isAdmin) { - return null; - } - return ( - -