Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
3d0ee00
feat: setup DomainAdminsPage
war-in Dec 9, 2025
38be466
add basic RHP with user search
sumo-slonik Dec 9, 2025
42251cb
add basic RHP with user search
sumo-slonik Dec 9, 2025
cb3aeda
feat: add DomainAdminDetailsPage
war-in Dec 9, 2025
cbac45b
feat: implement list of admins & search bar
war-in Dec 9, 2025
7894538
Merge branch 'refs/heads/main' into war-in/add-domain-admins-page
war-in Dec 9, 2025
aa90ca6
fix: move header buttons down on narrow layout
war-in Dec 9, 2025
7bda2be
Working selection RHP
sumo-slonik Dec 9, 2025
e3bf258
Merge branch 'war-in/add-domain-admins-page' into feature/kuba-nowako…
sumo-slonik Dec 9, 2025
54d35f0
add settings RHP for admins
sumo-slonik Dec 9, 2025
e991e3a
feat: integrate admins data into onyx
war-in Dec 9, 2025
3dd04cf
add placeholder for primary contact RHP
sumo-slonik Dec 9, 2025
30d4f87
add navigation after selecting primary contact
sumo-slonik Dec 9, 2025
ac16fff
Merge remote-tracking branch 'origin/war-in/add-domain-admins-page' i…
war-in Dec 9, 2025
e31fa9e
chore: fix prettier
war-in Dec 10, 2025
5b7ae32
wip: start adding optimistic onyx data
war-in Dec 10, 2025
68e0a0d
feat: revoke admin access optimistic and failure paths
war-in Dec 10, 2025
b8f4eb5
refactor: improve the settingsPage
war-in Dec 10, 2025
de0e4d1
feat: add pending actions & error handling
war-in Dec 11, 2025
93f5e14
add onyx connection for add admin (without banding actions)
sumo-slonik Dec 11, 2025
617f4de
Merge remote-tracking branch 'origin/war-in/add-domain-admins-page' i…
sumo-slonik Dec 11, 2025
c5d238b
filter addAdmins tab by current used admins
sumo-slonik Dec 11, 2025
fe3a64e
feat: revoke admin access & remove domain flows
war-in Dec 11, 2025
b535c6e
fix adding admin again after removing
sumo-slonik Dec 11, 2025
dead3d8
handling pending actions and errors on admins page
sumo-slonik Dec 11, 2025
61a7d0f
fixed domain actions
sumo-slonik Dec 11, 2025
f9d0cc1
feat: add reset domain flow
war-in Dec 11, 2025
4a49459
code refactor in progress
sumo-slonik Dec 11, 2025
5e7c3ad
refactor routes
sumo-slonik Dec 11, 2025
a97ee86
run prettier
sumo-slonik Dec 11, 2025
3f431f9
Merge remote-tracking branch 'origin/war-in/add-domain-admins-page' i…
war-in Dec 11, 2025
8378f25
feat: connect two commands
war-in Dec 11, 2025
dfbea87
fix not found page bug
sumo-slonik Dec 11, 2025
9a516a6
Merge remote-tracking branch 'origin/war-in/add-domain-admins-page' i…
sumo-slonik Dec 11, 2025
59dbb20
add comments to code
sumo-slonik Dec 11, 2025
05f5b4e
init members page
sumo-slonik Dec 12, 2025
8b67989
connect domain members with onyx
sumo-slonik Dec 12, 2025
f53104b
remove button
sumo-slonik Dec 12, 2025
52e3b2b
add member details page
sumo-slonik Dec 12, 2025
9be9ec7
add member addMember
sumo-slonik Dec 15, 2025
51bcbe8
work in progress
sumo-slonik Dec 16, 2025
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
2 changes: 1 addition & 1 deletion Mobile-Expensify
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,6 +719,17 @@ const ONYXKEYS = {

/** SAML login metadata for a domain */
SAML_METADATA: 'saml_metadata_',

/** Domain admin permissions */
DOMAIN_ADMIN_PERMISSIONS: 'expensify_adminPermissions_',

/** Pending actions for a domain */
DOMAIN_PENDING_ACTIONS: 'domainPendingActions_',

/** Errors related to a domain */
DOMAIN_ERRORS: 'domainErrors_',

DOMAIN_SECURITY_GROUP: 'expensify_securityGroup',
},

/** List of Form ids */
Expand Down Expand Up @@ -925,6 +936,7 @@ const ONYXKEYS = {
ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT: 'enableGlobalReimbursementsFormDraft',
CREATE_DOMAIN_FORM: 'createDomainForm',
CREATE_DOMAIN_FORM_DRAFT: 'createDomainFormDraft',
RESET_DOMAIN_FORM: 'resetDomainForm',
},
DERIVED: {
REPORT_ATTRIBUTES: 'reportAttributes',
Expand Down Expand Up @@ -1040,6 +1052,7 @@ type OnyxFormValuesMapping = {
[ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm;
[ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm;
[ONYXKEYS.FORMS.CREATE_DOMAIN_FORM]: FormTypes.CreateDomainForm;
[ONYXKEYS.FORMS.RESET_DOMAIN_FORM]: FormTypes.ResetDomainForm;
};

type OnyxFormDraftValuesMapping = {
Expand Down Expand Up @@ -1105,6 +1118,9 @@ 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.DOMAIN_PENDING_ACTIONS]: OnyxTypes.DomainPendingActions;
[ONYXKEYS.COLLECTION.DOMAIN_ERRORS]: OnyxTypes.DomainErrors;
};

type OnyxValuesMapping = {
Expand Down
36 changes: 36 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3424,6 +3424,42 @@ 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,
},
DOMAIN_ADD_ADMIN: {
route: 'domain/:accountID/admins/invite',
getRoute: (accountID: number) => `domain/${accountID}/admins/invite` as const,
},
DOMAIN_ADMIN_DETAILS: {
route: 'domain/:domainAccountID/admins/:accountID',
getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/admins/${accountID}` 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,
},
DOMAIN_RESET_DOMAIN: {
route: 'domain/:domainAccountID/admins/:accountID/reset-domain',
getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/admins/${accountID}/reset-domain` as const,
},
DOMAIN_MEMBERS: {
route: 'domain/:accountID/members',
getRoute: (accountID: number) => `domain/${accountID}/members` as const,
},
DOMAIN_MEMBER_DETAILS: {
route: 'domain/:domainAccountID/members/:accountID',
getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/members/${accountID}` as const,
},
DOMAIN_ADD_MEMBER: {
route: 'domain/:accountID/members/invite',
getRoute: (accountID: number) => `domain/${accountID}/members/invite` as const,
},
} as const;

/**
Expand Down
9 changes: 9 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,15 @@ const SCREENS = {
VERIFIED: 'Domain_Verified',
INITIAL: 'Domain_Initial',
SAML: 'Domain_SAML',
ADMINS: 'Domain_Admins',
ADD_ADMIN: 'Domain_Add_Admin',
ADMIN_DETAILS: 'Domain_Admin_Details',
ADMINS_SETTINGS: 'Admins_Settings',
ADD_PRIMARY_CONTACT: 'Add_Primary_Contact',
RESET_DOMAIN: 'Reset_Domain',
MEMBERS: 'Domain_Members',
MEMBER_DETAILS:'Member_Details',
ADD_MEMBER: 'Domain_Add_Member',
},
} as const;

Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/chunks/illustrations.chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,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';
Expand Down Expand Up @@ -271,6 +272,7 @@ const Illustrations = {
Mailbox,
ShieldYellow,
Clock,
Members,
};

/**
Expand Down
18 changes: 18 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7757,6 +7757,24 @@ 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',
addAdmin: 'Add admin',
findAdmin: 'Find admin',
revokeAdminAccess: 'Revoke admin access',
resetDomain: 'Reset domain',
invite: 'Invite',
settings: 'Settings',
addPrimaryContact: 'Add primary contact',
resetDomainExplanation: 'Please enter your domain to confirm you wish to reset the domain. Your domain name is:',
enterDomainName: 'Enter your domain name here',
},
members:{
title: 'Members',
findMember: 'Find member',
addMember: 'Add member',
invite: 'Invite',
}
},
};

Expand Down
7 changes: 7 additions & 0 deletions src/libs/API/parameters/AddAdminToDomainParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type AddAdminToDomainParams = {
authToken?: string | null;
domainName: string;
targetEmail: string;
};

export default AddAdminToDomainParams;
7 changes: 7 additions & 0 deletions src/libs/API/parameters/AddMemberToDomainParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
type AddMemberToDomainParams = {
authToken?: string | null;
domainName: string;
targetEmail: string;
};

export default AddMemberToDomainParams;
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
type SetConsolidatedDomainBillingEnabledParams = {
authToken?: string | null;
domainAccountID: number;
domainName: string;
enabled: boolean;
};

export default SetConsolidatedDomainBillingEnabledParams;
4 changes: 3 additions & 1 deletion src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,4 +445,6 @@ export type {default as DomainParams} from './DomainParams';
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 AddAdminToDomainParams} from './AddAdminToDomainParams';
export type {default as AddMemberToDomainParams} from './AddMemberToDomainParams';
export type {default as SetConsolidatedDomainBillingEnabledParams} from './SetConsolidatedDomainBillingEnabledParams';
6 changes: 6 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,9 @@ const WRITE_COMMANDS = {
UPDATE_SAML_ENABLED: 'UpdateSAMLEnabled',
UPDATE_SAML_REQUIRED: 'UpdateSAMLRequired',
CREATE_DOMAIN: 'CreateDomain',
ADD_DOMAIN_ADMIN: 'AddDomainAdmin',
ADD_DOMAIN_MEMBER: 'AddDomainMember',
SET_CONSOLIDATED_DOMAIN_BILLING_ENABLED: 'SetConsolidatedDomainBillingEnabled',
} as const;

type WriteCommand = ValueOf<typeof WRITE_COMMANDS>;
Expand Down Expand Up @@ -1069,6 +1072,9 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UPDATE_SAML_ENABLED]: Parameters.UpdateSamlEnabledParams;
[WRITE_COMMANDS.UPDATE_SAML_REQUIRED]: Parameters.UpdateSamlRequiredParams;
[WRITE_COMMANDS.CREATE_DOMAIN]: Parameters.DomainParams;
[WRITE_COMMANDS.ADD_DOMAIN_ADMIN]: Parameters.AddAdminToDomainParams;
[WRITE_COMMANDS.ADD_DOMAIN_MEMBER]: Parameters.AddMemberToDomainParams;
[WRITE_COMMANDS.SET_CONSOLIDATED_DOMAIN_BILLING_ENABLED]: Parameters.SetConsolidatedDomainBillingEnabledParams;
};

const READ_COMMANDS = {
Expand Down
65 changes: 65 additions & 0 deletions src/libs/DomainUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
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<number>()
);
}

/**
* Finds the specific key in the domain object that corresponds to a given admin's accountID.
*
* @param domain - The domain object from Onyx
* @param accountID - The account ID of the admin
* @returns The key string (e.g. 'expensify_adminPermissions_<NUMBER>') or undefined if not found
*/
function getAdminKey(domain: OnyxTypes.Domain | undefined, accountID: number): string | undefined {
if (!domain) {
return undefined;
}

return Object.entries(domain).find(([key, value]) => {
return key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_ADMIN_PERMISSIONS) && Number(value) === accountID;
})?.[0];
}

function selectMemberIDs(domain: OnyxTypes.Domain | undefined): number[] {
if (!domain) {
return [];
}

const memberIDs = Object.entries(domain)
.filter(([key]) => key.startsWith(ONYXKEYS.COLLECTION.DOMAIN_SECURITY_GROUP))
.flatMap(([, value]) => {
const groupData = value as { shared?: Record<string, string> };
if (!groupData?.shared) {
return [];
}
return Object.keys(groupData.shared);
})
.map((id) => Number(id))
.filter((id) => !Number.isNaN(id));
return [...new Set(memberIDs)];
}

export {selectAdminIDs, getAdminKey, selectMemberIDs};
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,13 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/receiptPartners/ChangeReceiptBillingAccountPage').default,
[SCREENS.DOMAIN.VERIFY]: () => require<ReactComponentModule>('../../../../pages/domain/SamlVerifyDomainPage').default,
[SCREENS.DOMAIN.VERIFIED]: () => require<ReactComponentModule>('../../../../pages/domain/SamlDomainVerifiedPage').default,
[SCREENS.DOMAIN.ADD_ADMIN]: () => require<ReactComponentModule>('../../../../pages/domain/admins/DomainAddAdminPage').default,
[SCREENS.DOMAIN.ADMINS_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/domain/admins/SettingsPage').default,
[SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: () => require<ReactComponentModule>('@pages/domain/admins/AddPrimaryContactPage').default,
[SCREENS.DOMAIN.ADMIN_DETAILS]: () => require<ReactComponentModule>('../../../../pages/domain/DomainAdminDetailsPage').default,
[SCREENS.DOMAIN.RESET_DOMAIN]: () => require<ReactComponentModule>('@pages/domain/DomainResetDomainPage').default,
[SCREENS.DOMAIN.MEMBER_DETAILS]: () => require<ReactComponentModule>('../../../../pages/domain/members/MembersDetailsPage').default,
[SCREENS.DOMAIN.ADD_MEMBER]: () => require<ReactComponentModule>('../../../../pages/domain/members/DomainAddMemberPage').default,
});

const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator<EnablePaymentsNavigatorParamList>({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import type ReactComponentModule from '@src/types/utils/ReactComponentModule';

const loadDomainInitialPage = () => require<ReactComponentModule>('../../../../pages/domain/DomainInitialPage').default;
const loadDomainSamlPage = () => require<ReactComponentModule>('../../../../pages/domain/DomainSamlPage').default;
const loadDomainAdminsPage = () => require<ReactComponentModule>('../../../../pages/domain/DomainAdminsPage').default;
const loadDomainMembersPage = () => require<ReactComponentModule>('../../../../pages/domain/members/DomainMembersPage').default;


const Split = createSplitNavigator<DomainSplitNavigatorParamList>();

Expand Down Expand Up @@ -43,6 +46,18 @@ function DomainSplitNavigator({route, navigation}: PlatformStackScreenProps<Auth
name={SCREENS.DOMAIN.SAML}
getComponent={loadDomainSamlPage}
/>

<Split.Screen
key={SCREENS.DOMAIN.ADMINS}
name={SCREENS.DOMAIN.ADMINS}
getComponent={loadDomainAdminsPage}
/>

<Split.Screen
key={SCREENS.DOMAIN.MEMBERS}
name={SCREENS.DOMAIN.MEMBERS}
getComponent={loadDomainMembersPage}
/>
</Split.Navigator>
</View>
</FocusTrapForScreens>
Expand Down
2 changes: 2 additions & 0 deletions src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import SCREENS from '@src/SCREENS';
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.ADD_ADMIN, SCREENS.DOMAIN.ADMIN_DETAILS, SCREENS.DOMAIN.ADMINS_SETTINGS, SCREENS.DOMAIN.ADD_PRIMARY_CONTACT, SCREENS.DOMAIN.RESET_DOMAIN],
[SCREENS.DOMAIN.MEMBERS]: [SCREENS.DOMAIN.MEMBER_DETAILS,SCREENS.DOMAIN.ADD_MEMBER],
};

export default DOMAIN_TO_RHP;
27 changes: 27 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1095,9 +1095,30 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.DOMAIN.VERIFY]: {
path: ROUTES.DOMAIN_VERIFY.route,
},
[SCREENS.DOMAIN.ADD_ADMIN]: {
path: ROUTES.DOMAIN_ADD_ADMIN.route,
},
[SCREENS.DOMAIN.ADD_PRIMARY_CONTACT]: {
path: ROUTES.DOMAIN_ADD_PRIMARY_CONTACT.route,
},
[SCREENS.DOMAIN.VERIFIED]: {
path: ROUTES.DOMAIN_VERIFIED.route,
},
[SCREENS.DOMAIN.ADMIN_DETAILS]: {
path: ROUTES.DOMAIN_ADMIN_DETAILS.route,
},
[SCREENS.DOMAIN.ADMINS_SETTINGS]: {
path: ROUTES.DOMAIN_ADMINS_SETTINGS.route,
},
[SCREENS.DOMAIN.RESET_DOMAIN]: {
path: ROUTES.DOMAIN_RESET_DOMAIN.route,
},
[SCREENS.DOMAIN.MEMBER_DETAILS]: {
path: ROUTES.DOMAIN_MEMBER_DETAILS.route,
},
[SCREENS.DOMAIN.ADD_MEMBER]: {
path: ROUTES.DOMAIN_ADD_MEMBER.route,
},
},
},
[SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: {
Expand Down Expand Up @@ -1911,6 +1932,12 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.DOMAIN.SAML]: {
path: ROUTES.DOMAIN_SAML.route,
},
[SCREENS.DOMAIN.ADMINS]: {
path: ROUTES.DOMAIN_ADMINS.route,
},
[SCREENS.DOMAIN.MEMBERS]: {
path: ROUTES.DOMAIN_MEMBERS.route,
},
},
},

Expand Down
Loading
Loading