From d7be725ae9ab2fa64cf41eeac907b0ebc189d2c3 Mon Sep 17 00:00:00 2001 From: war-in Date: Fri, 12 Dec 2025 10:18:23 -0500 Subject: [PATCH 01/17] feat: add DomainAdminsSettingsPage.tsx & DomainAddPrimaryContactPage.tsx --- src/ONYXKEYS.ts | 8 ++ src/ROUTES.ts | 8 ++ src/SCREENS.ts | 2 + src/languages/en.ts | 3 + .../ModalStackNavigators/index.tsx | 2 + .../linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 6 + src/libs/Navigation/types.ts | 6 + src/libs/actions/Domain.ts | 58 +++++++++ .../Admins/DomainAddPrimaryContactPage.tsx | 119 ++++++++++++++++++ src/pages/domain/Admins/DomainAdminsPage.tsx | 28 ++++- .../Admins/DomainAdminsSettingsPage.tsx | 64 ++++++++++ src/types/onyx/CardFeeds.ts | 3 + src/types/onyx/DomainErrors.ts | 13 ++ src/types/onyx/DomainPendingActions.ts | 13 ++ src/types/onyx/index.ts | 4 + 16 files changed, 336 insertions(+), 2 deletions(-) create mode 100644 src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx create mode 100644 src/pages/domain/Admins/DomainAdminsSettingsPage.tsx create mode 100644 src/types/onyx/DomainErrors.ts create mode 100644 src/types/onyx/DomainPendingActions.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 82f698af8c69..64a57a85e101 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -734,6 +734,12 @@ const ONYXKEYS = { /** */ DOMAIN_ADMIN_PERMISSIONS: 'expensify_adminPermissions_', + + /** Pending actions for a domain */ + DOMAIN_PENDING_ACTIONS: 'domainPendingActions_', + + /** Errors related to a domain */ + DOMAIN_ERRORS: 'domainErrors_', }, /** List of Form ids */ @@ -1121,6 +1127,8 @@ type OnyxCollectionValuesMapping = { [ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS]: boolean; [ONYXKEYS.COLLECTION.SAML_METADATA]: OnyxTypes.SamlMetadata; [ONYXKEYS.COLLECTION.DOMAIN_ADMIN_PERMISSIONS]: number; + [ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions; + [ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors; }; type OnyxValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2debc94d7156..7c9a04231ada 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3460,6 +3460,14 @@ const ROUTES = { route: 'domain/:accountID/admins', getRoute: (accountID: number) => `domain/${accountID}/admins` as const, }, + DOMAIN_ADMINS_SETTINGS: { + route: 'domain/:accountID/admins/settings', + getRoute: (accountID: number) => `domain/${accountID}/admins/settings` as const, + }, + DOMAIN_ADD_PRIMARY_CONTACT: { + route: 'domain/:accountID/admins/settings/primary-contact', + getRoute: (accountID: number) => `domain/${accountID}/admins/settings/primary-contact` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4a89c75481cd..f8f797683cc9 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -853,6 +853,8 @@ const SCREENS = { INITIAL: 'Domain_Initial', SAML: 'Domain_SAML', ADMINS: 'Domain_Admins', + ADMINS_SETTINGS: 'Admins_Settings', + ADD_PRIMARY_CONTACT: 'Add_Primary_Contact', }, } as const; diff --git a/src/languages/en.ts b/src/languages/en.ts index 146642d84696..1c4fce820345 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7851,6 +7851,9 @@ const translations = { admins: { title: 'Admins', findAdmin: 'Find admin', + primaryContact: 'Primary contact', + addPrimaryContact: 'Add primary contact', + settings: 'Settings', }, }, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 9dd9725e6e18..e41f60e0f18e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -918,6 +918,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/receiptPartners/ChangeReceiptBillingAccountPage').default, [SCREENS.DOMAIN.VERIFY]: () => require('../../../../pages/domain/SamlVerifyDomainPage').default, [SCREENS.DOMAIN.VERIFIED]: () => require('../../../../pages/domain/SamlDomainVerifiedPage').default, + [SCREENS.DOMAIN.ADMINS_SETTINGS]: () => require('../../../../pages/domain/admins/DomainAdminsSettingsPage').default, + [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: () => require('@pages/domain/admins/DomainAddPrimaryContactPage').default, }); const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator({ diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts index b4b63e99d5b8..dda03185f279 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts @@ -5,6 +5,7 @@ import SCREENS from '@src/SCREENS'; const DOMAIN_TO_RHP: Partial> = { [SCREENS.DOMAIN.INITIAL]: [], [SCREENS.DOMAIN.SAML]: [SCREENS.DOMAIN.VERIFY, SCREENS.DOMAIN.VERIFIED], + [SCREENS.DOMAIN.ADMINS]: [SCREENS.DOMAIN.ADMINS_SETTINGS, SCREENS.DOMAIN.ADD_PRIMARY_CONTACT], }; export default DOMAIN_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 953755ce7ffc..0e07758f315e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1105,6 +1105,12 @@ const config: LinkingOptions['config'] = { [SCREENS.DOMAIN.VERIFIED]: { path: ROUTES.DOMAIN_VERIFIED.route, }, + [SCREENS.DOMAIN.ADMINS_SETTINGS]: { + path: ROUTES.DOMAIN_ADMINS_SETTINGS.route, + }, + [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: { + path: ROUTES.DOMAIN_ADD_PRIMARY_CONTACT.route, + }, }, }, [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8b5538a33b3b..c47993b4e348 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1324,6 +1324,12 @@ type SettingsNavigatorParamList = { [SCREENS.DOMAIN.VERIFIED]: { accountID: number; }; + [SCREENS.DOMAIN.ADMINS_SETTINGS]: { + accountID: number; + }; + [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: { + accountID: number; + }; } & ReimbursementAccountNavigatorParamList; type DomainCardNavigatorParamList = { diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts index 4c2d982dd090..8d943ce8c700 100644 --- a/src/libs/actions/Domain.ts +++ b/src/libs/actions/Domain.ts @@ -351,6 +351,62 @@ function resetCreateDomainForm() { Onyx.merge(ONYXKEYS.FORMS.CREATE_DOMAIN_FORM, null); } +function choosePrimaryContact(domainAccountID: number, newTechnicalContactEmail: string | null, currentTechnicalContactEmail?: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, + value: { + settings: { + technicalContactEmail: newTechnicalContactEmail, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, + value: { + technicalContactEmail: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, + value: { + technicalContactEmail: null, + }, + }, + ]; + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, + value: { + settings: { + technicalContactEmail: currentTechnicalContactEmail, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN_PENDING_ACTIONS}${domainAccountID}`, + value: { + technicalContactEmail: null, + }, + }, + ]; + + // API.write(); +} + +function clearChoosePrimaryContactError(domainAccountID: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN_ERRORS}${domainAccountID}`, { + technicalContactEmailErrors: null, + }); +} + export { getDomainValidationCode, validateDomain, @@ -365,4 +421,6 @@ export { getScimToken, createDomain, resetCreateDomainForm, + choosePrimaryContact, + clearChoosePrimaryContactError, }; diff --git a/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx b/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx new file mode 100644 index 000000000000..a382064535e4 --- /dev/null +++ b/src/pages/domain/Admins/DomainAddPrimaryContactPage.tsx @@ -0,0 +1,119 @@ +import React, {useMemo} 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 SelectionList from '@components/SelectionList'; +import InviteMemberListItem from '@components/SelectionList/ListItem/InviteMemberListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import {selectAdminIDs} from '@libs/DomainUtils'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import tokenizedSearch from '@libs/tokenizedSearch'; +import Navigation from '@navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import {choosePrimaryContact} from '@userActions/Domain'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; + +type AdminOption = Omit & { + accountID: number; + login: string; +}; +type DomainAddPrimaryContactPageProps = PlatformStackScreenProps; + +function DomainAddPrimaryContactPage({route}: DomainAddPrimaryContactPageProps) { + const domainID = route.params.accountID; + const {translate, formatPhoneNumber, localeCompare} = useLocalize(); + const [adminIDs] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainID}`, { + canBeMissing: true, + selector: selectAdminIDs, + }); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: true}); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [countryCode = CONST.DEFAULT_COUNTRY_CODE] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); + const [domainSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainID}`, { + canBeMissing: false, + }); + const selectedUser = domainSettings?.settings?.technicalContactEmail; + let selectedKey: string | undefined; + + const data: AdminOption[] = []; + for (const accountID of adminIDs ?? []) { + const details = personalDetails?.[accountID]; + if (details?.login === selectedUser) { + selectedKey = String(accountID ?? ''); + } + data.push({ + isSelected: details?.login === selectedUser, + 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 filteredApprovers = + debouncedSearchTerm !== '' ? tokenizedSearch(data, getSearchValueForPhoneOrEmail(debouncedSearchTerm, countryCode), (option) => [option.text ?? '', option.login ?? '']) : data; + const filteredData = sortAlphabetically(filteredApprovers, 'text', localeCompare); + + const textInputOptions = useMemo( + () => ({ + label: translate('selectionList.findMember'), + value: searchTerm, + onChangeText: setSearchTerm, + headerMessage: searchTerm && !data?.length ? translate('common.noResultsFound') : '', + }), + [translate, searchTerm, setSearchTerm, data?.length], + ); + + return ( + + + + { + choosePrimaryContact(route.params.accountID, option.login === selectedUser ? null : (option.login ?? ''), selectedUser); + Navigation.goBack(); + }} + ListItem={InviteMemberListItem} + canSelectMultiple={false} + initiallyFocusedItemKey={selectedKey} + shouldScrollToFocusedIndex + shouldShowTextInput + textInputOptions={textInputOptions} + addBottomSafeAreaPadding + showScrollIndicator + /> + + + ); +} + +DomainAddPrimaryContactPage.displayName = 'DomainAddPrimaryContactPage'; + +export default DomainAddPrimaryContactPage; diff --git a/src/pages/domain/Admins/DomainAdminsPage.tsx b/src/pages/domain/Admins/DomainAdminsPage.tsx index 07c1c0695ded..9182393ce677 100644 --- a/src/pages/domain/Admins/DomainAdminsPage.tsx +++ b/src/pages/domain/Admins/DomainAdminsPage.tsx @@ -1,7 +1,9 @@ import React, {useCallback} from 'react'; +import {View} from 'react-native'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import Button from '@components/Button'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; +import {FallbackAvatar, Gear} from '@components/Icon/Expensicons'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; import SearchBar from '@components/SearchBar'; @@ -93,6 +95,25 @@ function DomainAdminsPage({route}: DomainAdminsPageProps) { ); }; + const getHeaderButtons = () => { + if (!isAdmin) { + return null; + } + return ( + +