Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3639,6 +3639,10 @@ const ROUTES = {
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,
},
} as const;

/**
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,7 @@ const SCREENS = {
ADD_PRIMARY_CONTACT: 'Add_Primary_Contact',
ADD_ADMIN: 'Domain_Add_Admin',
MEMBERS: 'Domain_Members',
MEMBER_DETAILS: 'Member_Details',
},
} as const;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.DOMAIN.ADMINS_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/domain/Admins/DomainAdminsSettingsPage').default,
[SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: () => require<ReactComponentModule>('../../../../pages/domain/Admins/DomainAddPrimaryContactPage').default,
[SCREENS.DOMAIN.ADD_ADMIN]: () => require<ReactComponentModule>('../../../../pages/domain/Admins/DomainAddAdminPage').default,
[SCREENS.DOMAIN.MEMBER_DETAILS]: () => require<ReactComponentModule>('../../../../pages/domain/Members/DomainMemberDetailsPage').default,
});

const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator<EnablePaymentsNavigatorParamList>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const DOMAIN_TO_RHP: Partial<Record<keyof DomainSplitNavigatorParamList, string[
[SCREENS.DOMAIN.INITIAL]: [],
[SCREENS.DOMAIN.SAML]: [SCREENS.DOMAIN.VERIFY, SCREENS.DOMAIN.VERIFIED],
[SCREENS.DOMAIN.ADMINS]: [SCREENS.DOMAIN.ADMIN_DETAILS, SCREENS.DOMAIN.ADMINS_SETTINGS, SCREENS.DOMAIN.ADD_PRIMARY_CONTACT, SCREENS.DOMAIN.ADD_ADMIN],
[SCREENS.DOMAIN.MEMBERS]: [SCREENS.DOMAIN.MEMBER_DETAILS],
};

export default DOMAIN_TO_RHP;
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: {
path: ROUTES.DOMAIN_ADD_PRIMARY_CONTACT.route,
},
[SCREENS.DOMAIN.MEMBER_DETAILS]: {
path: ROUTES.DOMAIN_MEMBER_DETAILS.route,
},
},
},
[SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: {
Expand Down
4 changes: 4 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1387,6 +1387,10 @@ type SettingsNavigatorParamList = {
[SCREENS.DOMAIN.ADD_ADMIN]: {
domainAccountID: number;
};
[SCREENS.DOMAIN.MEMBER_DETAILS]: {
domainAccountID: number;
accountID: number;
};
} & ReimbursementAccountNavigatorParamList;

type DomainCardNavigatorParamList = {
Expand Down
100 changes: 21 additions & 79 deletions src/pages/domain/Admins/DomainAdminDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,28 @@
import {Str} from 'expensify-common';
import {adminAccountIDsSelector, domainSettingsPrimaryContactSelector} from '@selectors/Domain';
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 {ModalActions} from '@components/Modal/Global/ModalContext';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useConfirmModal from '@hooks/useConfirmModal';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useThemeStyles from '@hooks/useThemeStyles';
import {revokeDomainAdminAccess} from '@libs/actions/Domain';
import {getDisplayNameOrDefault, getPhoneNumber} from '@libs/PersonalDetailsUtils';
import {getDisplayNameOrDefault} 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 BaseDomainMemberDetailsComponent from '@pages/domain/BaseDomainMemberDetailsComponent';
import {revokeDomainAdminAccess} from '@userActions/Domain';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import {adminAccountIDsSelector, domainSettingsPrimaryContactSelector} from '@src/selectors/Domain';
import type {PersonalDetailsList} from '@src/types/onyx';

type DomainAdminDetailsPageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.DOMAIN.ADMIN_DETAILS>;

function DomainAdminDetailsPage({route}: DomainAdminDetailsPageProps) {
const {domainAccountID, accountID} = route.params;

const styles = useThemeStyles();
const {translate, formatPhoneNumber} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Info', 'ClosedSign'] as const);
Expand All @@ -53,16 +43,12 @@ function DomainAdminDetailsPage({route}: DomainAdminDetailsPageProps) {
selector: (personalDetailsList: OnyxEntry<PersonalDetailsList>) => personalDetailsList?.[accountID],
});

const domainHasOnlyOneAdmin = adminAccountIDs?.length === 1;
const displayName = formatPhoneNumber(getDisplayNameOrDefault(adminPersonalDetails));
const memberLogin = adminPersonalDetails?.login ?? '';
const isCurrentUserPrimaryContact = primaryContact === memberLogin;
const isSMSLogin = Str.isSMSLogin(memberLogin);
const phoneNumber = getPhoneNumber(adminPersonalDetails);
const fallbackIcon = adminPersonalDetails?.fallbackIcon ?? '';

const domainHasOnlyOneAdmin = adminAccountIDs?.length === 1;
const {showConfirmModal} = useConfirmModal();

const handleRevokeAdminAccess = async () => {
const confirmResult = await showConfirmModal({
title: translate('domain.admins.revokeAdminAccess'),
Expand All @@ -82,65 +68,21 @@ function DomainAdminDetailsPage({route}: DomainAdminDetailsPageProps) {
};

return (
<DomainNotFoundPageWrapper domainAccountID={domainAccountID}>
<ScreenWrapper
enableEdgeToEdgeBottomSafeAreaPadding
testID={DomainAdminDetailsPage.displayName}
>
<HeaderWithBackButton title={displayName} />
<ScrollView addBottomSafeAreaPadding>
<View style={[styles.containerWithSpaceBetween, styles.pointerEventsBoxNone, styles.justifyContentStart]}>
<View style={[styles.avatarSectionWrapper, styles.pb0]}>
<OfflineWithFeedback pendingAction={adminPersonalDetails?.pendingFields?.avatar}>
<Avatar
containerStyles={[styles.avatarXLarge, styles.mb4, styles.noOutline]}
imageStyles={[styles.avatarXLarge]}
source={adminPersonalDetails?.avatar}
avatarID={accountID}
type={CONST.ICON_TYPE_AVATAR}
size={CONST.AVATAR_SIZE.X_LARGE}
fallbackIcon={fallbackIcon}
/>
</OfflineWithFeedback>
{!!displayName && (
<Text
style={[styles.textHeadline, styles.pre, styles.mb8, styles.w100, styles.textAlignCenter]}
numberOfLines={1}
>
{displayName}
</Text>
)}
</View>
<View style={styles.w100}>
<MenuItemWithTopDescription
title={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : memberLogin}
copyValue={isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : memberLogin}
description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
interactive={false}
copyable
/>
{!domainHasOnlyOneAdmin && (
<MenuItem
disabled={isCurrentUserPrimaryContact}
hintText={isCurrentUserPrimaryContact ? translate('domain.admins.cantRevokeAdminAccess') : undefined}
style={styles.mb5}
title={translate('domain.admins.revokeAdminAccess')}
icon={icons.ClosedSign}
onPress={handleRevokeAdminAccess}
/>
)}
<MenuItem
style={styles.mb5}
title={translate('common.profile')}
icon={icons.Info}
onPress={() => Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute()))}
shouldShowRightIcon
/>
</View>
</View>
</ScrollView>
</ScreenWrapper>
</DomainNotFoundPageWrapper>
<BaseDomainMemberDetailsComponent
domainAccountID={domainAccountID}
accountID={accountID}
>
{!domainHasOnlyOneAdmin && (
<MenuItem
disabled={isCurrentUserPrimaryContact}
hintText={isCurrentUserPrimaryContact ? translate('domain.admins.cantRevokeAdminAccess') : undefined}
style={styles.mb5}
title={translate('domain.admins.revokeAdminAccess')}
icon={icons.ClosedSign}
onPress={handleRevokeAdminAccess}
/>
)}
</BaseDomainMemberDetailsComponent>
);
}

Expand Down
113 changes: 113 additions & 0 deletions src/pages/domain/BaseDomainMemberDetailsComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
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 {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 CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {PersonalDetailsList} from '@src/types/onyx';
import DomainNotFoundPageWrapper from './DomainNotFoundPageWrapper';

type BaseDomainMemberDetailsComponentProps = {
/** Domain ID */
domainAccountID: number;

/** User account ID */
accountID: number;

/** List of additional fields (e.g., force 2FA) */
children?: React.ReactNode;
};

function BaseDomainMemberDetailsComponent({domainAccountID, accountID, children}: BaseDomainMemberDetailsComponentProps) {
const styles = useThemeStyles();
const {translate, formatPhoneNumber} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Info']);

// The selector depends on the dynamic `accountID`, so it cannot be extracted
// to a static function outside the component.
// eslint-disable-next-line rulesdir/no-inline-useOnyx-selector
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For all eslint disablings, there should be comment explaining why.
Can we move selector function to the top of the file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no we can't, we need the accountID

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add the comment

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we discussed this in the past and there was a way to achieve this somehow 🤔

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
canBeMissing: true,
selector: (personalDetailsList: OnyxEntry<PersonalDetailsList>) => personalDetailsList?.[accountID],
});

const displayName = formatPhoneNumber(getDisplayNameOrDefault(personalDetails));
const phoneNumber = getPhoneNumber(personalDetails);
const memberLogin = personalDetails?.login ?? '';
const isSMSLogin = Str.isSMSLogin(memberLogin);
const copyableName = isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : memberLogin;

return (
<DomainNotFoundPageWrapper domainAccountID={domainAccountID}>
<ScreenWrapper
enableEdgeToEdgeBottomSafeAreaPadding
testID={BaseDomainMemberDetailsComponent.displayName}
>
<HeaderWithBackButton title={displayName} />

<ScrollView addBottomSafeAreaPadding>
<View style={[styles.containerWithSpaceBetween, styles.pointerEventsBoxNone, styles.justifyContentStart]}>
<View style={[styles.avatarSectionWrapper, styles.pb0]}>
<OfflineWithFeedback pendingAction={personalDetails?.pendingFields?.avatar}>
<Avatar
containerStyles={[styles.avatarXLarge, styles.mb4, styles.noOutline]}
imageStyles={[styles.avatarXLarge]}
source={personalDetails?.avatar}
avatarID={accountID}
type={CONST.ICON_TYPE_AVATAR}
size={CONST.AVATAR_SIZE.X_LARGE}
fallbackIcon={personalDetails?.fallbackIcon}
/>
</OfflineWithFeedback>

{!!displayName && (
<Text
style={[styles.textHeadline, styles.pre, styles.mb8, styles.w100, styles.textAlignCenter]}
numberOfLines={1}
>
{displayName}
</Text>
)}
</View>
<View style={styles.w100}>
<MenuItemWithTopDescription
title={copyableName}
copyValue={copyableName}
description={translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
interactive={false}
copyable
/>
{children}
<MenuItem
style={styles.mb5}
title={translate('common.profile')}
icon={icons.Info}
onPress={() => Navigation.navigate(ROUTES.PROFILE.getRoute(accountID, Navigation.getActiveRoute()))}
shouldShowRightIcon
/>
</View>
</View>
</ScrollView>
</ScreenWrapper>
</DomainNotFoundPageWrapper>
);
}

BaseDomainMemberDetailsComponent.displayName = 'BaseDomainMemberDetailsComponent';

export default BaseDomainMemberDetailsComponent;
22 changes: 22 additions & 0 deletions src/pages/domain/Members/DomainMemberDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';
import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@navigation/types';
import BaseDomainMemberDetailsComponent from '@pages/domain/BaseDomainMemberDetailsComponent';
import type SCREENS from '@src/SCREENS';

type DomainMemberDetailsPageProps = PlatformStackScreenProps<SettingsNavigatorParamList, typeof SCREENS.DOMAIN.MEMBER_DETAILS>;

function DomainMemberDetailsPage({route}: DomainMemberDetailsPageProps) {
const {domainAccountID, accountID} = route.params;

return (
<BaseDomainMemberDetailsComponent
domainAccountID={domainAccountID}
accountID={accountID}
/>
);
}

DomainMemberDetailsPage.displayName = 'DomainMemberDetailsPage';

export default DomainMemberDetailsPage;
4 changes: 3 additions & 1 deletion src/pages/domain/Members/DomainMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ import React from 'react';
import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
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';

type DomainMembersPageProps = PlatformStackScreenProps<DomainSplitNavigatorParamList, typeof SCREENS.DOMAIN.MEMBERS>;
Expand All @@ -27,7 +29,7 @@ function DomainMembersPage({route}: DomainMembersPageProps) {
accountIDs={memberIDs ?? []}
headerTitle={translate('domain.members.title')}
searchPlaceholder={translate('domain.members.findMember')}
onSelectRow={() => {}}
onSelectRow={(item) => Navigation.navigate(ROUTES.DOMAIN_MEMBER_DETAILS.getRoute(domainAccountID, item.accountID))}
headerIcon={illustrations.Profile}
/>
);
Expand Down
Loading