From 7d5ea4fea8fb2693164c2b2d1e97539b8919439b Mon Sep 17 00:00:00 2001 From: sumo_slonik Date: Mon, 22 Dec 2025 10:56:48 +0100 Subject: [PATCH 01/47] add members details page --- src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/languages/en.ts | 2 + .../API/parameters/AddMemberToDomainParams.ts | 7 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 3 + .../ModalStackNavigators/index.tsx | 1 + .../linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + .../domain/Members/DomainAddMemberPage.tsx | 96 +++++++++++++++++++ .../domain/Members/DomainMembersPage.tsx | 34 +++++-- 12 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 src/libs/API/parameters/AddMemberToDomainParams.ts create mode 100644 src/pages/domain/Members/DomainAddMemberPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2cc4af13b3dd..5ebdcc5de5f8 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3477,6 +3477,10 @@ const ROUTES = { route: 'domain/:domainAccountID/members', getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members` as const, }, + DOMAIN_ADD_MEMBER: { + route: 'domain/:domainAccountID/members/invite', + getRoute: (domainAccountID: number) => `domain/${domainAccountID}/members/invite` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 993cd9709d1a..cc5f0945c8fb 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -859,6 +859,7 @@ const SCREENS = { ADMINS_SETTINGS: 'Admins_Settings', ADD_PRIMARY_CONTACT: 'Add_Primary_Contact', MEMBERS: 'Domain_Members', + ADD_MEMBER: 'Domain_Add_Member', }, } as const; diff --git a/src/languages/en.ts b/src/languages/en.ts index 493bff423e3e..3a0a71ebb34d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7828,6 +7828,8 @@ const translations = { members:{ title: 'Members', findMember: 'Find member', + addMember: 'Add member', + invite: 'Invite', } }, }; diff --git a/src/libs/API/parameters/AddMemberToDomainParams.ts b/src/libs/API/parameters/AddMemberToDomainParams.ts new file mode 100644 index 000000000000..9eed560b48ae --- /dev/null +++ b/src/libs/API/parameters/AddMemberToDomainParams.ts @@ -0,0 +1,7 @@ +type AddMemberToDomainParams = { + authToken?: string | null; + domainName: string; + targetEmail: string; +}; + +export default AddMemberToDomainParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f27feccfaf62..1124b1a151e9 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -448,5 +448,6 @@ export type {default as GetScimTokenParams} from './GetScimTokenParams'; export type {default as SetSamlIdentityParams} from './SetSamlIdentityParams'; export type {default as UpdateSamlEnabledParams} from './UpdateSamlEnabledParams'; export type {default as UpdateSamlRequiredParams} from './UpdateSamlRequiredParams'; +export type {default as AddMemberToDomainParams} from './AddMemberToDomainParams'; export type {default as SetPolicyRequireCompanyCardsEnabledParams} from './SetPolicyRequireCompanyCardsEnabled'; export type {default as SetTechnicalContactEmailParams} from './SetTechnicalContactEmailParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 2da2814ba411..281268fa9a54 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -527,7 +527,9 @@ const WRITE_COMMANDS = { UPDATE_SAML_ENABLED: 'UpdateSAMLEnabled', UPDATE_SAML_REQUIRED: 'UpdateSAMLRequired', CREATE_DOMAIN: 'CreateDomain', + ADD_DOMAIN_MEMBER: 'AddDomainMember', SET_TECHNICAL_CONTACT_EMAIL: 'SetTechnicalContactEmail', + } as const; type WriteCommand = ValueOf; @@ -1077,6 +1079,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_SAML_REQUIRED]: Parameters.UpdateSamlRequiredParams; [WRITE_COMMANDS.CREATE_DOMAIN]: Parameters.DomainParams; [WRITE_COMMANDS.SET_TECHNICAL_CONTACT_EMAIL]: Parameters.SetTechnicalContactEmailParams; + [WRITE_COMMANDS.ADD_DOMAIN_MEMBER]: Parameters.AddMemberToDomainParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 85642f25d618..d2eb47a96ae9 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -847,6 +847,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/domain/Admins/DomainAdminDetailsPage').default, [SCREENS.DOMAIN.ADMINS_SETTINGS]: () => require('../../../../pages/domain/Admins/DomainAdminsSettingsPage').default, [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: () => require('../../../../pages/domain/Admins/DomainAddPrimaryContactPage').default, + [SCREENS.DOMAIN.ADD_MEMBER]: () => require('../../../../pages/domain/members/DomainAddMemberPage').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 ea0c54caf54d..5ee266e6f59e 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.ADD_MEMBER]: { + path: ROUTES.DOMAIN_ADD_MEMBER.route, + }, }, }, [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fa8cb7072f86..4819f2b05ddf 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1342,6 +1342,9 @@ type SettingsNavigatorParamList = { [SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: { domainAccountID: number; }; + [SCREENS.DOMAIN.ADD_MEMBER]: { + accountID: number; + }; } & ReimbursementAccountNavigatorParamList; type DomainCardNavigatorParamList = { diff --git a/src/pages/domain/Members/DomainAddMemberPage.tsx b/src/pages/domain/Members/DomainAddMemberPage.tsx new file mode 100644 index 000000000000..372d9a87823a --- /dev/null +++ b/src/pages/domain/Members/DomainAddMemberPage.tsx @@ -0,0 +1,96 @@ +import {Str} from 'expensify-common'; +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type DomainAddMemberProps = PlatformStackScreenProps; + +function DomainAddMemberPage({route}: DomainAddMemberProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const domainAccountID = route.params.accountID; + const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, {canBeMissing: true}); + + + const domainName = domain ? Str.extractEmailDomain(domain.email) : undefined; + const domainSuffix = useMemo(() => (domainName ? `@${domainName}` : ''), [domainName]); + const [email, setEmail] = useState(domainSuffix); + + const handleInputChange = useCallback((value: string) => { + if (!domainName) { + setEmail(value); + return; + } + + const loginPart = value.replace(domainSuffix, '').split('@').at(0); + + if (loginPart === '') { + setEmail(''); + } else { + setEmail(`${loginPart}${domainSuffix}`); + } + }, [domainName, domainSuffix]); + + const inviteUser = useCallback(() => { + + }, [email, domainSuffix]); + + const isButtonDisabled = !email || email === domainSuffix || !email.includes('@'); + + return ( + + { + Navigation.goBack(ROUTES.DOMAIN_ADMINS.getRoute(domainAccountID)); + }} + /> + + + + + + + + ); +} + +DomainAddMemberPage.displayName = 'DomainAddMemberPage'; + +export default DomainAddMemberPage; diff --git a/src/pages/domain/Members/DomainMembersPage.tsx b/src/pages/domain/Members/DomainMembersPage.tsx index 01a02f2e0312..d66183b4c704 100644 --- a/src/pages/domain/Members/DomainMembersPage.tsx +++ b/src/pages/domain/Members/DomainMembersPage.tsx @@ -1,27 +1,48 @@ +import { selectMemberIDs } from '@selectors/Domain'; import React from 'react'; -import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; +import { useMemoizedLazyIllustrations } from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; -import type {DomainSplitNavigatorParamList} from '@navigation/types'; +import Navigation from '@navigation/Navigation'; +import type { PlatformStackScreenProps } from '@navigation/PlatformStackNavigation/types'; +import type { DomainSplitNavigatorParamList } from '@navigation/types'; +import BaseDomainMembersPage from '@pages/domain/BaseDomainMembersPage'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import {selectMemberIDs} from '@selectors/Domain'; -import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; +import Button from '@components/Button'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; + type DomainMembersPageProps = PlatformStackScreenProps; function DomainMembersPage({route}: DomainMembersPageProps) { const {domainAccountID} = route.params; const {translate} = useLocalize(); - const illustrations = useMemoizedLazyIllustrations(['Members']); + const illustrations = useMemoizedLazyIllustrations(['Members','Plus'] as const); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const styles = useThemeStyles(); const [memberIDs,memberMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${domainAccountID}`, { canBeMissing: true, selector: selectMemberIDs, }); + const renderHeaderButtons = ( +