diff --git a/assets/images/laptop-on-desk-with-coffee-and-key.svg b/assets/images/laptop-on-desk-with-coffee-and-key.svg
new file mode 100644
index 000000000000..eb74f071c210
--- /dev/null
+++ b/assets/images/laptop-on-desk-with-coffee-and-key.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 0785b360f025..c56d395725f2 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1025,6 +1025,7 @@ const CONST = {
MERGE_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/settings/Merge-Accounts',
CONNECT_A_BUSINESS_BANK_ACCOUNT_HELP_URL: 'https://help.expensify.com/articles/new-expensify/expenses-&-payments/Connect-a-Business-Bank-Account',
DOMAIN_VERIFICATION_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/domains/Claim-And-Verify-A-Domain',
+ SAML_HELP_URL: 'https://help.expensify.com/articles/expensify-classic/domains/Managing-Single-Sign-On-(SSO)-in-Expensify',
REGISTER_FOR_WEBINAR_URL: 'https://events.zoom.us/eo/Aif1I8qCi1GZ7KnLnd1vwGPmeukSRoPjFpyFAZ2udQWn0-B86e1Z~AggLXsr32QYFjq8BlYLZ5I06Dg',
TEST_RECEIPT_URL: `${CLOUDFRONT_URL}/images/fake-receipt__tacotodds.png`,
// Use Environment.getEnvironmentURL to get the complete URL with port number
@@ -5382,6 +5383,7 @@ const CONST = {
/** These action types are custom for RootNavigator */
DISMISS_MODAL: 'DISMISS_MODAL',
OPEN_WORKSPACE_SPLIT: 'OPEN_WORKSPACE_SPLIT',
+ OPEN_DOMAIN_SPLIT: 'OPEN_DOMAIN_SPLIT',
SET_HISTORY_PARAM: 'SET_HISTORY_PARAM',
REPLACE_PARAMS: 'REPLACE_PARAMS',
TOGGLE_SIDE_PANEL_WITH_HISTORY: 'TOGGLE_SIDE_PANEL_WITH_HISTORY',
diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts
index 354cea71889e..9e75a1468e1a 100644
--- a/src/NAVIGATORS.ts
+++ b/src/NAVIGATORS.ts
@@ -14,6 +14,7 @@ export default {
REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator',
SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator',
WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator',
+ DOMAIN_SPLIT_NAVIGATOR: 'DomainSplitNavigator',
SEARCH_FULLSCREEN_NAVIGATOR: 'SearchFullscreenNavigator',
SHARE_MODAL_NAVIGATOR: 'ShareModalNavigator',
PUBLIC_RIGHT_MODAL_NAVIGATOR: 'PublicRightModalNavigator',
diff --git a/src/ROUTES.ts b/src/ROUTES.ts
index c761a5c0eef4..76eb75bde14f 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -3367,6 +3367,22 @@ const ROUTES = {
route: 'workspaces/domain-verified/:accountID',
getRoute: (accountID: number) => `workspaces/domain-verified/${accountID}` as const,
},
+ DOMAIN_INITIAL: {
+ route: 'domain/:accountID',
+ getRoute: (accountID: number) => `domain/${accountID}` as const,
+ },
+ DOMAIN_SAML: {
+ route: 'domain/:accountID/saml',
+ getRoute: (accountID: number) => `domain/${accountID}/saml` as const,
+ },
+ DOMAIN_VERIFY: {
+ route: 'domain/:accountID/verify',
+ getRoute: (accountID: number) => `domain/${accountID}/verify` as const,
+ },
+ DOMAIN_VERIFIED: {
+ route: 'domain/:accountID/verified',
+ getRoute: (accountID: number) => `domain/${accountID}/verified` as const,
+ },
} as const;
/**
diff --git a/src/SCREENS.ts b/src/SCREENS.ts
index 2cbb45f73c79..168ec1c433c8 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -824,6 +824,12 @@ const SCREENS = {
},
WORKSPACES_VERIFY_DOMAIN: 'Workspaces_Verify_Domain',
WORKSPACES_DOMAIN_VERIFIED: 'Workspaces_Domain_Verified',
+ DOMAIN: {
+ VERIFY: 'Domain_Verify',
+ VERIFIED: 'Domain_Verified',
+ INITIAL: 'Domain_Initial',
+ SAML: 'Domain_SAML',
+ },
} as const;
type Screen = DeepValueOf;
diff --git a/src/components/Domain/DomainMenuItem.tsx b/src/components/Domain/DomainMenuItem.tsx
index 7bdef86bbe05..0af6c5e5f8ee 100644
--- a/src/components/Domain/DomainMenuItem.tsx
+++ b/src/components/Domain/DomainMenuItem.tsx
@@ -1,10 +1,12 @@
import React from 'react';
+import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import type {OfflineWithFeedbackProps} from '@components/OfflineWithFeedback';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import type {PopoverMenuItem} from '@components/PopoverMenu';
import {PressableWithoutFeedback} from '@components/Pressable';
import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
@@ -43,10 +45,16 @@ function DomainMenuItem({item, index}: DomainMenuItemProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isAdmin, isValidated} = item;
+ const theme = useTheme();
const threeDotsMenuItems: PopoverMenuItem[] | undefined =
!isValidated && isAdmin
? [
+ {
+ icon: Expensicons.Globe,
+ text: translate('domain.goToDomain'),
+ onSelected: item.action,
+ },
{
icon: Expensicons.Globe,
text: translate('domain.verifyDomain.title'),
@@ -74,6 +82,23 @@ function DomainMenuItem({item, index}: DomainMenuItemProps) {
badgeText={isAdmin && !isValidated ? translate('domain.notVerified') : undefined}
isHovered={hovered}
menuItems={threeDotsMenuItems}
+ rightIcon={
+ isValidated ? (
+
+ ) : (
+
+ )
+ }
/>
)}
diff --git a/src/components/Domain/DomainsListRow.tsx b/src/components/Domain/DomainsListRow.tsx
index adfe7d9b2377..fe344b717cb3 100644
--- a/src/components/Domain/DomainsListRow.tsx
+++ b/src/components/Domain/DomainsListRow.tsx
@@ -1,3 +1,4 @@
+import type {ReactElement} from 'react';
import React from 'react';
import {View} from 'react-native';
import type {ValueOf} from 'type-fest';
@@ -24,11 +25,14 @@ type DomainsListRowProps = {
/** Items for the three dots menu */
menuItems?: PopoverMenuItem[];
- /** The type of brick road indicator to show. */
+ /** The type of brick road indicator to show */
brickRoadIndicator?: ValueOf;
+
+ /** Icon to display at the end of the row */
+ rightIcon: ReactElement;
};
-function DomainsListRow({title, isHovered, badgeText, brickRoadIndicator, menuItems}: DomainsListRowProps) {
+function DomainsListRow({title, isHovered, badgeText, brickRoadIndicator, menuItems, rightIcon}: DomainsListRowProps) {
const styles = useThemeStyles();
const theme = useTheme();
@@ -79,13 +83,7 @@ function DomainsListRow({title, isHovered, badgeText, brickRoadIndicator, menuIt
)}
-
-
-
+ {rightIcon}
);
diff --git a/src/components/FeatureList.tsx b/src/components/FeatureList.tsx
index da9144fbb3f3..34f73a295d39 100644
--- a/src/components/FeatureList.tsx
+++ b/src/components/FeatureList.tsx
@@ -24,6 +24,9 @@ type FeatureListProps = {
/** The text to display in the subtitle of the section */
subtitle?: string;
+ /** The component to display custom subtitle */
+ renderSubtitle?: () => ReactNode;
+
/** Text of the call to action button */
ctaText?: string;
@@ -76,6 +79,7 @@ function FeatureList({
contentPaddingOnLargeScreens,
footer,
isButtonDisabled = false,
+ renderSubtitle,
}: FeatureListProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
@@ -92,6 +96,7 @@ function FeatureList({
titleStyles={titleStyles}
illustrationContainerStyle={illustrationContainerStyle}
contentPaddingOnLargeScreens={contentPaddingOnLargeScreens}
+ renderSubtitle={renderSubtitle}
>
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index 405cec53fd24..456e4ae1a055 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -25,6 +25,7 @@ import CompanyCardsPendingState from '@assets/images/companyCards/pendingstate_l
import Computer from '@assets/images/computer.svg';
import EmptyCardState from '@assets/images/emptystate__expensifycard.svg';
import ExpensifyCardImage from '@assets/images/expensify-card.svg';
+import LaptopOnDeskWithCoffeeAndKey from '@assets/images/laptop-on-desk-with-coffee-and-key.svg';
import LaptopWithSecondScreenAndHourglass from '@assets/images/laptop-with-second-screen-and-hourglass.svg';
import LaptopWithSecondScreenSync from '@assets/images/laptop-with-second-screen-sync.svg';
import LaptopWithSecondScreenX from '@assets/images/laptop-with-second-screen-x.svg';
@@ -85,6 +86,7 @@ import Luggage from '@assets/images/simple-illustrations/simple-illustration__lu
import Mailbox from '@assets/images/simple-illustrations/simple-illustration__mailbox.svg';
import ExpensifyMobileApp from '@assets/images/simple-illustrations/simple-illustration__mobileapp.svg';
import MoneyIntoWallet from '@assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg';
+import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__opensafe.svg';
import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg';
import PaperAirplane from '@assets/images/simple-illustrations/simple-illustration__paperairplane.svg';
import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg';
@@ -226,4 +228,6 @@ export {
CardReplacementSuccess,
EmptyShelves,
BlueShield,
+ OpenSafe,
+ LaptopOnDeskWithCoffeeAndKey,
};
diff --git a/src/components/Navigation/NavigationTabBar/index.tsx b/src/components/Navigation/NavigationTabBar/index.tsx
index 9e638426cabb..d1f6edf831bd 100644
--- a/src/components/Navigation/NavigationTabBar/index.tsx
+++ b/src/components/Navigation/NavigationTabBar/index.tsx
@@ -35,7 +35,7 @@ import {buildCannedSearchQuery, buildSearchQueryJSON, buildSearchQueryString} fr
import type {BrickRoad} from '@libs/WorkspacesSettingsUtils';
import {getChatTabBrickRoad} from '@libs/WorkspacesSettingsUtils';
import navigationRef from '@navigation/navigationRef';
-import type {RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types';
+import type {DomainSplitNavigatorParamList, RootNavigatorParamList, SearchFullscreenNavigatorParamList, State, WorkspaceSplitNavigatorParamList} from '@navigation/types';
import NavigationTabBarAvatar from '@pages/home/sidebar/NavigationTabBarAvatar';
import NavigationTabBarFloatingActionButton from '@pages/home/sidebar/NavigationTabBarFloatingActionButton';
import variables from '@styles/variables';
@@ -44,7 +44,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import type {Policy} from '@src/types/onyx';
+import type {Domain, Policy} from '@src/types/onyx';
import NAVIGATION_TABS from './NAVIGATION_TABS';
type NavigationTabBarProps = {
@@ -78,20 +78,25 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
const initialNavigationRouteState = getWorkspaceNavigationRouteState();
const [lastWorkspacesTabNavigatorRoute, setLastWorkspacesTabNavigatorRoute] = useState(initialNavigationRouteState.lastWorkspacesTabNavigatorRoute);
const [workspacesTabState, setWorkspacesTabState] = useState(initialNavigationRouteState.workspacesTabState);
- const params = workspacesTabState?.routes?.at(0)?.params as WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL];
+ const params = workspacesTabState?.routes?.at(0)?.params as
+ | WorkspaceSplitNavigatorParamList[typeof SCREENS.WORKSPACE.INITIAL]
+ | DomainSplitNavigatorParamList[typeof SCREENS.DOMAIN.INITIAL];
const {typeMenuSections} = useSearchTypeMenuSections();
const subscriptionPlan = useSubscriptionPlan();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['ExpensifyAppIcon', 'Inbox', 'MoneySearch', 'Buildings'] as const);
+ const paramsPolicyID = params && 'policyID' in params ? params.policyID : undefined;
+ const paramsDomainAccountID = params && 'accountID' in params ? params.accountID : undefined;
+
const lastViewedPolicySelector = useCallback(
(policies: OnyxCollection) => {
- if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !params?.policyID) {
+ if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || !paramsPolicyID) {
return undefined;
}
- return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${params.policyID}`];
+ return policies?.[`${ONYXKEYS.COLLECTION.POLICY}${paramsPolicyID}`];
},
- [params?.policyID, lastWorkspacesTabNavigatorRoute],
+ [paramsPolicyID, lastWorkspacesTabNavigatorRoute],
);
const [lastViewedPolicy] = useOnyx(
@@ -103,6 +108,26 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
[navigationState],
);
+ const lastViewedDomainSelector = useCallback(
+ (domains: OnyxCollection) => {
+ if (!lastWorkspacesTabNavigatorRoute || lastWorkspacesTabNavigatorRoute.name !== NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR || !paramsDomainAccountID) {
+ return undefined;
+ }
+
+ return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`];
+ },
+ [paramsDomainAccountID, lastWorkspacesTabNavigatorRoute],
+ );
+
+ const [lastViewedDomain] = useOnyx(
+ ONYXKEYS.COLLECTION.DOMAIN,
+ {
+ canBeMissing: true,
+ selector: lastViewedDomainSelector,
+ },
+ [navigationState],
+ );
+
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {selector: reportsSelector, canBeMissing: true});
const {login: currentUserLogin} = useCurrentUserPersonalDetails();
const {shouldUseNarrowLayout} = useResponsiveLayout();
@@ -189,8 +214,8 @@ function NavigationTabBar({selectedTab, isTopLevelBar = false, shouldShowFloatin
* If the user clicks on the settings tab while on this tab, this button should go back to the previous screen within the tab.
*/
const showWorkspaces = useCallback(() => {
- navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy});
- }, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy]);
+ navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain});
+ }, [shouldUseNarrowLayout, currentUserLogin, lastViewedPolicy, lastViewedDomain]);
if (!shouldUseNarrowLayout) {
return (
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 2ad61d75c1b4..7f2218844071 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7426,6 +7426,16 @@ ${amount} für ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`Die Domain ${domainName} wurde erfolgreich verifiziert und Sie können jetzt SAML und andere Sicherheitsfunktionen einrichten.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'SAML-Einmalanmeldung (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO ist eine Sicherheitsfunktion, die Ihnen mehr Kontrolle darüber gibt, wie sich Mitglieder mit E-Mail-Adressen unter ${domainName} bei Expensify anmelden. Um sie zu aktivieren, müssen Sie sich als autorisierte/r Unternehmensadministrator/in verifizieren.`,
+ fasterAndEasierLogin: 'Schnelleres und einfacheres Anmelden',
+ moreSecurityAndControl: 'Mehr Sicherheit und Kontrolle',
+ onePasswordForAnything: 'Ein Passwort für alles',
+ },
+ goToDomain: 'Zur Domain wechseln',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 7f693e2893ea..d313d6d3c93e 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -7367,6 +7367,16 @@ const translations = {
description: ({domainName}: {domainName: string}) =>
`The domain ${domainName} has been successfully verified and you can now set up SAML and other security features.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'SAML Single Sign-On (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO is a security feature that gives you more control over how members with ${domainName} emails log into Expensify. To enable it, you'll need to verify yourself as an authorized company admin.`,
+ fasterAndEasierLogin: 'Faster and easier login',
+ moreSecurityAndControl: 'More security and control',
+ onePasswordForAnything: 'One password for everything',
+ },
+ goToDomain: 'Go to domain',
},
};
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 07ae997f4072..86815d1dc836 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -7508,6 +7508,16 @@ ${amount} para ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`El dominio ${domainName} se ha verificado correctamente y ahora puedes configurar SAML y otras funciones de seguridad.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'Inicio de sesión único SAML (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO es una función de seguridad que te da más control sobre cómo los miembros con correos ${domainName} inician sesión en Expensify. Para habilitarla, deberás verificarte como administrador autorizado de la empresa.`,
+ fasterAndEasierLogin: 'Inicio de sesión más rápido y sencillo',
+ moreSecurityAndControl: 'Más seguridad y control',
+ onePasswordForAnything: 'Una sola contraseña para todo',
+ },
+ goToDomain: 'Ir al dominio',
},
};
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 35bbcb702fb9..6cf9548e435b 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7430,6 +7430,16 @@ ${amount} pour ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`Le domaine ${domainName} a été vérifié avec succès et vous pouvez maintenant configurer SAML et d'autres fonctionnalités de sécurité.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'Authentification unique SAML (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO est une fonctionnalité de sécurité qui vous donne davantage de contrôle sur la manière dont les membres ayant des adresses e‑mail ${domainName} se connectent à Expensify. Pour l’activer, vous devrez confirmer que vous êtes un administrateur d’entreprise autorisé.`,
+ fasterAndEasierLogin: 'Connexion plus rapide et plus simple',
+ moreSecurityAndControl: 'Plus de sécurité et de contrôle',
+ onePasswordForAnything: 'Un seul mot de passe pour tout',
+ },
+ goToDomain: 'Accéder au domaine',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 72a5662a227a..28d9194a5b27 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -7437,6 +7437,16 @@ ${amount} per ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`Il dominio ${domainName} è stato verificato con successo e ora puoi configurare SAML e altre funzionalità di sicurezza.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'SAML Accesso singolo (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO è una funzionalità di sicurezza che ti offre un maggiore controllo su come i membri con indirizzi email ${domainName} accedono a Expensify. Per abilitarla, dovrai verificare di essere un amministratore aziendale autorizzato.`,
+ fasterAndEasierLogin: 'Accesso più rapido e semplice',
+ moreSecurityAndControl: 'Maggiore sicurezza e controllo',
+ onePasswordForAnything: 'Un’unica password per tutto',
+ },
+ goToDomain: 'Vai al dominio',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index 2de963977cd2..6b8755efc674 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -7361,6 +7361,16 @@ ${date} - ${merchant}に${amount}`,
description: ({domainName}: {domainName: string}) =>
`ドメイン ${domainName} は正常に検証され、SAML やその他のセキュリティ機能を設定できるようになりました。`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'SAML シングルサインオン (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO は、${domainName} のメールアドレスを持つメンバーのExpensifyへのログイン方法をより細かく管理できるセキュリティ機能です。有効にするには、権限のある会社の管理者であることを確認する必要があります。`,
+ fasterAndEasierLogin: 'より速く、より簡単なログイン',
+ moreSecurityAndControl: 'さらなるセキュリティと管理',
+ onePasswordForAnything: 'すべてを1つのパスワードで',
+ },
+ goToDomain: 'ドメインに移動',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index bf84e013b5db..19eaccf573fd 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -7410,6 +7410,16 @@ ${amount} voor ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`Het domein ${domainName} is succesvol geverifieerd en je kunt nu SAML en andere beveiligingsfuncties instellen.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'SAML eenmalige aanmelding (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO is een beveiligingsfunctie die je meer controle geeft over hoe leden met e-mailadressen van ${domainName} inloggen bij Expensify. Om dit in te schakelen, moet je bevestigen dat je een geautoriseerde bedrijfsbeheerder bent.`,
+ fasterAndEasierLogin: 'Sneller en makkelijker inloggen',
+ moreSecurityAndControl: 'Meer beveiliging en controle',
+ onePasswordForAnything: 'Eén wachtwoord voor alles',
+ },
+ goToDomain: 'Ga naar het domein',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index f69024abe283..cc336d7d4ce0 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -7398,6 +7398,16 @@ ${amount} dla ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`Domena ${domainName} została pomyślnie zweryfikowana i możesz teraz skonfigurować SAML oraz inne funkcje zabezpieczeń.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'Jednokrotne logowanie SAML (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO to funkcja bezpieczeństwa, która daje Ci większą kontrolę nad tym, w jaki sposób członkowie z adresami e‑mail w domenie ${domainName} logują się do Expensify. Aby ją włączyć, musisz potwierdzić, że jesteś uprawnionym administratorem firmy.`,
+ fasterAndEasierLogin: 'Szybsze i łatwiejsze logowanie',
+ moreSecurityAndControl: 'Więcej bezpieczeństwa i kontroli',
+ onePasswordForAnything: 'Jedno hasło do wszystkiego',
+ },
+ goToDomain: 'Przejdź do domeny',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 014e67edc258..ff62e619d28c 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -7416,6 +7416,16 @@ ${amount} para ${merchant} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`O domínio ${domainName} foi verificado com sucesso e agora você pode configurar SAML e outros recursos de segurança.`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'Logon único SAML (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO é um recurso de segurança que oferece mais controle sobre como os membros com e-mails do domínio ${domainName} fazem login no Expensify. Para ativá-lo, você precisará confirmar sua identidade como um administrador autorizado da empresa.`,
+ fasterAndEasierLogin: 'Login mais rápido e fácil',
+ moreSecurityAndControl: 'Mais segurança e controle',
+ onePasswordForAnything: 'Uma senha para tudo',
+ },
+ goToDomain: 'Ir para o domínio',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 814ab712a38d..fe3cbd8fdf24 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -7243,6 +7243,16 @@ ${merchant}的${amount} - ${date}`,
description: ({domainName}: {domainName: string}) =>
`域名 ${domainName} 已成功验证,您现在可以设置 SAML 和其他安全功能。`,
},
+ saml: 'SAML',
+ samlFeatureList: {
+ title: 'SAML 单点登录 (SSO)',
+ subtitle: ({domainName}: {domainName: string}) =>
+ `SAML SSO 是一项安全功能,可让您更好地控制使用 ${domainName} 邮箱的成员如何登录 Expensify。要启用它,您需要验证自己是授权的公司管理员。`,
+ fasterAndEasierLogin: '更快、更简单的登录',
+ moreSecurityAndControl: '更强的安全性与控制',
+ onePasswordForAnything: '一个密码搞定一切',
+ },
+ goToDomain: '前往域',
},
};
// IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts,
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index d4b46ec8acf6..a1b32d5f28d2 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -62,7 +62,7 @@ import SCREENS from '@src/SCREENS';
import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
import attachmentModalScreenOptions from './attachmentModalScreenOptions';
import createRootStackNavigator from './createRootStackNavigator';
-import {screensWithEnteringAnimation, workspaceSplitsWithoutEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers';
+import {screensWithEnteringAnimation, workspaceOrDomainSplitsWithoutEnteringAnimation} from './createRootStackNavigator/GetStateForActionHandlers';
import defaultScreenOptions from './defaultScreenOptions';
import {ShareModalStackNavigator} from './ModalStackNavigators';
import ExplanationModalNavigator from './Navigators/ExplanationModalNavigator';
@@ -87,6 +87,7 @@ const loadWorkspaceJoinUser = () => require('@pages/worksp
const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default;
const loadSettingsSplitNavigator = () => require('./Navigators/SettingsSplitNavigator').default;
const loadWorkspaceSplitNavigator = () => require('./Navigators/WorkspaceSplitNavigator').default;
+const loadDomainSplitNavigator = () => require('./Navigators/DomainSplitNavigator').default;
const loadSearchNavigator = () => require('./Navigators/SearchFullscreenNavigator').default;
function initializePusher() {
@@ -356,7 +357,7 @@ function AuthScreens() {
}, [dismissToWideReport, modal?.disableDismissOnEscape, modal?.willAlertModalBecomeVisible, shouldRenderSecondaryOverlay]);
// Animation is disabled when navigating to the sidebar screen
- const getWorkspaceSplitNavigatorOptions = ({route}: {route: RouteProp}) => {
+ const getWorkspaceOrDomainSplitNavigatorOptions = ({route}: {route: RouteProp}) => {
// We don't need to do anything special for the wide screen.
if (!shouldUseNarrowLayout) {
return rootNavigatorScreenOptions.splitNavigator;
@@ -366,7 +367,7 @@ function AuthScreens() {
// If it is opened from other tab, we don't want to animate it on the entry.
// There is a hook inside the workspace navigator that changes animation to SLIDE_FROM_RIGHT after entering.
// This way it can be animated properly when going back to the settings split.
- const animationEnabled = !workspaceSplitsWithoutEnteringAnimation.has(route.key);
+ const animationEnabled = !workspaceOrDomainSplitsWithoutEnteringAnimation.has(route.key);
return {
...rootNavigatorScreenOptions.splitNavigator,
@@ -474,6 +475,7 @@ function AuthScreens() {
NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR,
NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
+ NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR,
NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR,
NAVIGATORS.RIGHT_MODAL_NAVIGATOR,
SCREENS.WORKSPACES_LIST,
@@ -498,9 +500,14 @@ function AuthScreens() {
/>
+
require('../../../../pages/workspace/receiptPartners/InviteReceiptPartnerPolicyPage').default,
[SCREENS.WORKSPACE.RECEIPT_PARTNERS_INVITE_EDIT]: () => require('../../../../pages/workspace/receiptPartners/EditInviteReceiptPartnerPolicyPage').default,
[SCREENS.WORKSPACE.RECEIPT_PARTNERS_CHANGE_BILLING_ACCOUNT]: () => require('../../../../pages/workspace/receiptPartners/ChangeReceiptBillingAccountPage').default,
+ [SCREENS.DOMAIN.VERIFY]: () => require('../../../../pages/domain/SamlVerifyDomainPage').default,
+ [SCREENS.DOMAIN.VERIFIED]: () => require('../../../../pages/domain/SamlDomainVerifiedPage').default,
});
const TwoFactorAuthenticatorStackNavigator = createModalStackNavigator({
@@ -931,8 +933,8 @@ const ScheduleCallModalStackNavigator = createModalStackNavigator({
- [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/VerifyDomainPage').default,
- [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/DomainVerifiedPage').default,
+ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/WorkspacesVerifyDomainPage').default,
+ [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/WorkspacesDomainVerifiedPage').default,
});
export {
diff --git a/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx
new file mode 100644
index 000000000000..b470a57476ed
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/DomainSplitNavigator.tsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import {View} from 'react-native';
+import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
+import useThemeStyles from '@hooks/useThemeStyles';
+import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
+import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
+import useNoAnimationWhenOpenedFromTabBar from '@libs/Navigation/helpers/useNoAnimationWhenOpenedFromTabBar';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {AuthScreensParamList, DomainSplitNavigatorParamList} from '@libs/Navigation/types';
+import type NAVIGATORS from '@src/NAVIGATORS';
+import SCREENS from '@src/SCREENS';
+import type ReactComponentModule from '@src/types/utils/ReactComponentModule';
+
+const loadDomainInitialPage = () => require('../../../../pages/domain/DomainInitialPage').default;
+const loadDomainSamlPage = () => require('../../../../pages/domain/DomainSamlPage').default;
+
+const Split = createSplitNavigator();
+
+function DomainSplitNavigator({route, navigation}: PlatformStackScreenProps) {
+ const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
+ const styles = useThemeStyles();
+
+ useNoAnimationWhenOpenedFromTabBar(navigation, route.key);
+
+ return (
+
+
+
+
+
+
+
+
+
+ );
+}
+
+DomainSplitNavigator.displayName = 'DomainSplitNavigator';
+
+export default DomainSplitNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
index 1f160ec8281b..fa63be0e6328 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/WorkspaceSplitNavigator.tsx
@@ -1,11 +1,11 @@
-import React, {useEffect} from 'react';
+import React from 'react';
import {View} from 'react-native';
import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen';
-import {workspaceSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers';
+import useThemeStyles from '@hooks/useThemeStyles';
import createSplitNavigator from '@libs/Navigation/AppNavigator/createSplitNavigator';
import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators';
import useSplitNavigatorScreenOptions from '@libs/Navigation/AppNavigator/useSplitNavigatorScreenOptions';
-import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
+import useNoAnimationWhenOpenedFromTabBar from '@libs/Navigation/helpers/useNoAnimationWhenOpenedFromTabBar';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {AuthScreensParamList, WorkspaceSplitNavigatorParamList} from '@libs/Navigation/types';
import type NAVIGATORS from '@src/NAVIGATORS';
@@ -39,30 +39,16 @@ const Split = createSplitNavigator();
function WorkspaceSplitNavigator({route, navigation}: PlatformStackScreenProps) {
const splitNavigatorScreenOptions = useSplitNavigatorScreenOptions();
+ const styles = useThemeStyles();
// This hook preloads the screens of adjacent tabs to make changing tabs faster.
usePreloadFullScreenNavigators();
- useEffect(() => {
- const unsubscribe = navigation.addListener('transitionEnd', () => {
- // We want to call this function only once.
- unsubscribe();
-
- // If we open this screen from a different tab, then it won't have animation.
- if (!workspaceSplitsWithoutEnteringAnimation.has(route.key)) {
- return;
- }
-
- // We want to set animation after mounting so it will animate on going UP to the settings split.
- navigation.setOptions({animation: Animations.SLIDE_FROM_RIGHT});
- });
-
- return unsubscribe;
- }, [navigation, route.key]);
+ useNoAnimationWhenOpenedFromTabBar(navigation, route.key);
return (
-
+
();
+const workspaceOrDomainSplitsWithoutEnteringAnimation = new Set();
const screensWithEnteringAnimation = new Set();
+
/**
- * Handles the OPEN_WORKSPACE_SPLIT action.
- * If the user is on other tab than workspaces and the workspace split is "remembered", this action will be called after pressing the settings tab.
- * It will push the workspace hub split navigator first and then push the workspace split navigator.
+ * Util function with common logic for handling OPEN_WORKSPACE_SPLIT and OPEN_DOMAIN_SPLIT actions.
+ *
+ * Pushes the workspace hub split navigator first and then pushes the split navigator.
* This allows the user to swipe back on the iOS to the workspace hub split navigator underneath.
*/
-function handleOpenWorkspaceSplitAction(
+function prepareStateUnderWorkspaceOrDomainNavigator(
state: StackNavigationState,
- action: OpenWorkspaceSplitActionType,
configOptions: RouterConfigOptions,
stackRouter: Router, CommonActions.Action | StackActionType>,
+ actionToPushWorkspaceSplitNavigator: StackActionType,
+ splitNavigatorName: typeof NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR | typeof NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR,
) {
const actionToPushWorkspacesList = StackActions.push(SCREENS.WORKSPACES_LIST);
const stateWithWorkspacesList = stackRouter.getStateForAction(state, actionToPushWorkspacesList, configOptions);
if (!stateWithWorkspacesList) {
- Log.hmmm('[handleOpenWorkspaceSplitAction] WorkspacesList has not been found in the navigation state.');
+ Log.hmmm('[handleOpenWorkspaceOrDomainSplitAction] WorkspacesList has not been found in the navigation state.');
return null;
}
- const actionToPushWorkspaceSplitNavigator = StackActions.push(NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, {
- screen: action.payload.screenName,
- params: {
- policyID: action.payload.policyID,
- },
- });
-
const rehydratedStateWithWorkspacesList = stackRouter.getRehydratedState(stateWithWorkspacesList, configOptions);
- const stateWithWorkspaceSplitNavigator = stackRouter.getStateForAction(rehydratedStateWithWorkspacesList, actionToPushWorkspaceSplitNavigator, configOptions);
+ const stateWithSplitNavigator = stackRouter.getStateForAction(rehydratedStateWithWorkspacesList, actionToPushWorkspaceSplitNavigator, configOptions);
- if (!stateWithWorkspaceSplitNavigator) {
- Log.hmmm('[handleOpenWorkspaceSplitAction] WorkspaceSplitNavigator has not been found in the navigation state.');
+ if (!stateWithSplitNavigator) {
+ Log.hmmm(`[handleOpenWorkspaceOrDomainSplitAction] ${splitNavigatorName} has not been found in the navigation state.`);
return null;
}
- const lastFullScreenRoute = stateWithWorkspaceSplitNavigator.routes.at(-1);
+ const lastFullScreenRoute = stateWithSplitNavigator.routes.at(-1);
if (lastFullScreenRoute?.key) {
- // If the user opened the workspace split navigator from a different tab, we don't want to animate the entering transition.
+ // If the user opened the workspace/domain split navigator from a different tab, we don't want to animate the entering transition.
// To make it feel like bottom tab navigator.
- workspaceSplitsWithoutEnteringAnimation.add(lastFullScreenRoute.key);
+ workspaceOrDomainSplitsWithoutEnteringAnimation.add(lastFullScreenRoute.key);
}
- return stateWithWorkspaceSplitNavigator;
+ return stateWithSplitNavigator;
+}
+
+/**
+ * Handles the OPEN_WORKSPACE_SPLIT action.
+ * If the user is on other tab than workspaces and the workspace split is "remembered", this action will be called after pressing the settings tab.
+ */
+function handleOpenWorkspaceSplitAction(
+ state: StackNavigationState,
+ action: OpenWorkspaceSplitActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+) {
+ const actionToPushWorkspaceSplitNavigator = StackActions.push(NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, {
+ screen: action.payload.screenName,
+ params: {
+ policyID: action.payload.policyID,
+ },
+ });
+
+ return prepareStateUnderWorkspaceOrDomainNavigator(state, configOptions, stackRouter, actionToPushWorkspaceSplitNavigator, NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR);
+}
+
+/**
+ * Handles the OPEN_DOMAIN_SPLIT action.
+ * If the user is on other tab than workspaces and the domain split is "remembered", this action will be called after pressing the settings tab.
+ */
+function handleOpenDomainSplitAction(
+ state: StackNavigationState,
+ action: OpenDomainSplitActionType,
+ configOptions: RouterConfigOptions,
+ stackRouter: Router, CommonActions.Action | StackActionType>,
+) {
+ const actionToPushDomainSplitNavigator = StackActions.push(NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR, {
+ screen: action.payload.screenName,
+ params: {
+ accountID: action.payload.accountID,
+ },
+ });
+
+ return prepareStateUnderWorkspaceOrDomainNavigator(state, configOptions, stackRouter, actionToPushDomainSplitNavigator, NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR);
}
function handlePushFullscreenAction(
@@ -194,9 +229,10 @@ export {
handleDismissModalAction,
handleNavigatingToModalFromModal,
handleOpenWorkspaceSplitAction,
+ handleOpenDomainSplitAction,
handlePushFullscreenAction,
handleReplaceReportsSplitNavigatorAction,
screensWithEnteringAnimation,
- workspaceSplitsWithoutEnteringAnimation,
+ workspaceOrDomainSplitsWithoutEnteringAnimation,
handleToggleSidePanelWithHistoryAction,
};
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts
index c22176c4b656..651bc15f14b1 100644
--- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/RootStackRouter.ts
@@ -9,6 +9,7 @@ import NAVIGATORS from '@src/NAVIGATORS';
import {
handleDismissModalAction,
handleNavigatingToModalFromModal,
+ handleOpenDomainSplitAction,
handleOpenWorkspaceSplitAction,
handlePushFullscreenAction,
handleReplaceReportsSplitNavigatorAction,
@@ -17,6 +18,7 @@ import {
import syncBrowserHistory from './syncBrowserHistory';
import type {
DismissModalActionType,
+ OpenDomainSplitActionType,
OpenWorkspaceSplitActionType,
PreloadActionType,
PushActionType,
@@ -30,6 +32,10 @@ function isOpenWorkspaceSplitAction(action: RootStackNavigatorAction): action is
return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
}
+function isOpenDomainSplitAction(action: RootStackNavigatorAction): action is OpenDomainSplitActionType {
+ return action.type === CONST.NAVIGATION.ACTION_TYPE.OPEN_DOMAIN_SPLIT;
+}
+
function isPushAction(action: RootStackNavigatorAction): action is PushActionType {
return action.type === CONST.NAVIGATION.ACTION_TYPE.PUSH;
}
@@ -96,6 +102,10 @@ function RootStackRouter(options: RootStackNavigatorRouterOptions) {
return handleOpenWorkspaceSplitAction(state, action, configOptions, stackRouter);
}
+ if (isOpenDomainSplitAction(action)) {
+ return handleOpenDomainSplitAction(state, action, configOptions, stackRouter);
+ }
+
if (isDismissModalAction(action)) {
return handleDismissModalAction(state, configOptions, stackRouter);
}
diff --git a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
index 155afa8c1399..72d4c5c330d5 100644
--- a/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
+++ b/src/libs/Navigation/AppNavigator/createRootStackNavigator/types.ts
@@ -1,5 +1,5 @@
import type {CommonActions, StackActionType, StackRouterOptions} from '@react-navigation/native';
-import type {WorkspaceScreenName} from '@libs/Navigation/types';
+import type {DomainScreenName, WorkspaceScreenName} from '@libs/Navigation/types';
import type CONST from '@src/CONST';
type RootStackNavigatorActionType =
@@ -19,6 +19,13 @@ type RootStackNavigatorActionType =
screenName: WorkspaceScreenName;
};
}
+ | {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_DOMAIN_SPLIT;
+ payload: {
+ accountID: number;
+ screenName: DomainScreenName;
+ };
+ }
| {
type: typeof CONST.NAVIGATION.ACTION_TYPE.PRELOAD;
payload: {
@@ -34,6 +41,10 @@ type OpenWorkspaceSplitActionType = RootStackNavigatorActionType & {
type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_WORKSPACE_SPLIT;
};
+type OpenDomainSplitActionType = RootStackNavigatorActionType & {
+ type: typeof CONST.NAVIGATION.ACTION_TYPE.OPEN_DOMAIN_SPLIT;
+};
+
type ToggleSidePanelWithHistoryActionType = RootStackNavigatorActionType & {
type: typeof CONST.NAVIGATION.ACTION_TYPE.TOGGLE_SIDE_PANEL_WITH_HISTORY;
};
@@ -54,6 +65,7 @@ type RootStackNavigatorAction = CommonActions.Action | StackActionType | RootSta
export type {
OpenWorkspaceSplitActionType,
+ OpenDomainSplitActionType,
PushActionType,
ReplaceActionType,
DismissModalActionType,
diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts
index b751f16aa407..16b094424a3e 100644
--- a/src/libs/Navigation/Navigation.ts
+++ b/src/libs/Navigation/Navigation.ts
@@ -375,7 +375,7 @@ function popToSidebar() {
const currentRouteName = currentRoute?.name as keyof typeof SPLIT_TO_SIDEBAR;
if (topRoute?.name !== SPLIT_TO_SIDEBAR[currentRouteName]) {
- const params = currentRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR ? {...lastRoute?.params} : undefined;
+ const params = currentRoute.name === NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR || currentRoute.name === NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR ? {...lastRoute?.params} : undefined;
const sidebarName = SPLIT_TO_SIDEBAR[currentRouteName];
diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
index 8f4b76aeceab..62c4de2a5e33 100644
--- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts
@@ -5,7 +5,7 @@ import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import getInitialSplitNavigatorState from '@libs/Navigation/AppNavigator/createSplitNavigator/getInitialSplitNavigatorState';
import {config} from '@libs/Navigation/linkingConfig/config';
-import {RHP_TO_SEARCH, RHP_TO_SETTINGS, RHP_TO_SIDEBAR, RHP_TO_WORKSPACE, RHP_TO_WORKSPACES_LIST} from '@libs/Navigation/linkingConfig/RELATIONS';
+import {RHP_TO_DOMAIN, RHP_TO_SEARCH, RHP_TO_SETTINGS, RHP_TO_SIDEBAR, RHP_TO_WORKSPACE, RHP_TO_WORKSPACES_LIST} from '@libs/Navigation/linkingConfig/RELATIONS';
import type {NavigationPartialRoute, RootNavigatorParamList} from '@libs/Navigation/types';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
@@ -126,6 +126,21 @@ function getMatchingFullScreenRoute(route: NavigationPartialRoute) {
);
}
+ if (RHP_TO_DOMAIN[route.name]) {
+ const paramsFromRoute = getParamsFromRoute(RHP_TO_DOMAIN[route.name]);
+
+ return getInitialSplitNavigatorState(
+ {
+ name: SCREENS.DOMAIN.INITIAL,
+ params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined,
+ },
+ {
+ name: RHP_TO_DOMAIN[route.name],
+ params: paramsFromRoute.length > 0 ? pick(route.params, paramsFromRoute) : undefined,
+ },
+ );
+ }
+
return undefined;
}
diff --git a/src/libs/Navigation/helpers/isNavigatorName.ts b/src/libs/Navigation/helpers/isNavigatorName.ts
index 1377a100200e..4fdebfb9bdf7 100644
--- a/src/libs/Navigation/helpers/isNavigatorName.ts
+++ b/src/libs/Navigation/helpers/isNavigatorName.ts
@@ -7,7 +7,7 @@ const FULL_SCREENS_SET = new Set([...Object.values(SIDEBAR_TO_SPLIT), NAVIGATORS
const SIDEBARS_SET = new Set(Object.values(SPLIT_TO_SIDEBAR));
const ONBOARDING_SCREENS_SET = new Set(Object.values(SCREENS.ONBOARDING));
const SPLIT_NAVIGATORS_SET = new Set(Object.values(SIDEBAR_TO_SPLIT));
-const WORKSPACES_TAB_SET = new Set(Object.values([NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, SCREENS.WORKSPACES_LIST]));
+const WORKSPACES_TAB_SET = new Set(Object.values([NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, SCREENS.WORKSPACES_LIST, NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR]));
/**
* Functions defined below are used to check whether a screen belongs to a specific group.
diff --git a/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts b/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
index 5efa27fd5fa0..77e28c8be21f 100644
--- a/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
+++ b/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts
@@ -8,7 +8,7 @@ import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
-import type {Policy} from '@src/types/onyx';
+import type {Domain, Policy} from '@src/types/onyx';
import {isFullScreenName, isWorkspacesTabScreenName} from './isNavigatorName';
import {getLastVisitedWorkspaceTabScreen, getWorkspacesTabStateFromSessionStorage} from './lastVisitedTabPathUtils';
@@ -16,6 +16,7 @@ type Params = {
currentUserLogin?: string;
shouldUseNarrowLayout: boolean;
policy?: Policy;
+ domain?: Domain;
};
// Gets the latest workspace navigation state, restoring from session or preserved state if needed.
@@ -48,7 +49,7 @@ const getWorkspaceNavigationRouteState = () => {
};
// Navigates to the appropriate workspace tab or workspace list page.
-const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy}: Params) => {
+const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain}: Params) => {
const {lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute} = getWorkspaceNavigationRouteState();
if (!topmostFullScreenRoute || topmostFullScreenRoute.name === SCREENS.WORKSPACES_LIST) {
@@ -91,6 +92,19 @@ const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, poli
return;
}
+ // Domain route found: try to restore last domain screen.
+ if (lastWorkspacesTabNavigatorRoute.name === NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR) {
+ // Restore to last-visited domain tab or show initial tab
+ if (domain?.accountID !== undefined) {
+ const domainScreenName = !shouldUseNarrowLayout ? getLastVisitedWorkspaceTabScreen() : SCREENS.DOMAIN.INITIAL;
+
+ return navigationRef.dispatch({
+ type: CONST.NAVIGATION.ACTION_TYPE.OPEN_DOMAIN_SPLIT,
+ payload: {accountID: domain.accountID, screenName: domainScreenName},
+ });
+ }
+ }
+
// Fallback: any other state, go to the list.
Navigation.navigate(ROUTES.WORKSPACES_LIST.route);
});
diff --git a/src/libs/Navigation/helpers/useNoAnimationWhenOpenedFromTabBar.ts b/src/libs/Navigation/helpers/useNoAnimationWhenOpenedFromTabBar.ts
new file mode 100644
index 000000000000..f4c1f2d3decc
--- /dev/null
+++ b/src/libs/Navigation/helpers/useNoAnimationWhenOpenedFromTabBar.ts
@@ -0,0 +1,35 @@
+import {useEffect} from 'react';
+import {workspaceOrDomainSplitsWithoutEnteringAnimation} from '@libs/Navigation/AppNavigator/createRootStackNavigator/GetStateForActionHandlers';
+import Animations from '@libs/Navigation/PlatformStackNavigation/navigationOptions/animation';
+import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {AuthScreensParamList} from '@libs/Navigation/types';
+import type NAVIGATORS from '@src/NAVIGATORS';
+
+/**
+ * Ensures that workspace/domain split navigator pages open without the animation
+ * when accessing them by selecting the Workspace tab in the navigation tab bar,
+ * to make it look like a bottom tab navigation.
+ */
+function useNoAnimationWhenOpenedFromTabBar(
+ navigation: PlatformStackNavigationProp,
+ routeKey: string,
+) {
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('transitionEnd', () => {
+ // We want to call this function only once.
+ unsubscribe();
+
+ // If we open this screen from a different tab, then it won't have animation.
+ if (!workspaceOrDomainSplitsWithoutEnteringAnimation.has(routeKey)) {
+ return;
+ }
+
+ // We want to set animation after mounting so it will animate on going UP to the settings split.
+ navigation.setOptions({animation: Animations.SLIDE_FROM_RIGHT});
+ });
+
+ return unsubscribe;
+ }, [navigation, routeKey]);
+}
+
+export default useNoAnimationWhenOpenedFromTabBar;
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
new file mode 100755
index 000000000000..b4b63e99d5b8
--- /dev/null
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
@@ -0,0 +1,10 @@
+import type {DomainSplitNavigatorParamList} from '@libs/Navigation/types';
+import SCREENS from '@src/SCREENS';
+
+// This file is used to define relation between domain split navigator's central screens and RHP screens.
+const DOMAIN_TO_RHP: Partial> = {
+ [SCREENS.DOMAIN.INITIAL]: [],
+ [SCREENS.DOMAIN.SAML]: [SCREENS.DOMAIN.VERIFY, SCREENS.DOMAIN.VERIFIED],
+};
+
+export default DOMAIN_TO_RHP;
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts
index c4d18632ca68..47f9ada98281 100644
--- a/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/SIDEBAR_TO_SPLIT.ts
@@ -6,6 +6,7 @@ const SIDEBAR_TO_SPLIT = {
[SCREENS.SETTINGS.ROOT]: NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR,
[SCREENS.HOME]: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR,
[SCREENS.WORKSPACE.INITIAL]: NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR,
+ [SCREENS.DOMAIN.INITIAL]: NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR,
};
export default SIDEBAR_TO_SPLIT;
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts b/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts
index b0dc3fc7025d..3fd187d043e2 100644
--- a/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/TAB_TO_FULLSCREEN.ts
@@ -8,7 +8,7 @@ const TAB_TO_FULLSCREEN: Record, FullScreenName[
[NAVIGATION_TABS.HOME]: [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR],
[NAVIGATION_TABS.SEARCH]: [NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR],
[NAVIGATION_TABS.SETTINGS]: [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR],
- [NAVIGATION_TABS.WORKSPACES]: [SCREENS.WORKSPACES_LIST, NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR],
+ [NAVIGATION_TABS.WORKSPACES]: [SCREENS.WORKSPACES_LIST, NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR, NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR],
};
export default TAB_TO_FULLSCREEN;
diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/index.ts b/src/libs/Navigation/linkingConfig/RELATIONS/index.ts
index 6ac7811ee3cf..2f0cb665c36c 100644
--- a/src/libs/Navigation/linkingConfig/RELATIONS/index.ts
+++ b/src/libs/Navigation/linkingConfig/RELATIONS/index.ts
@@ -1,3 +1,4 @@
+import DOMAIN_TO_RHP from './DOMAIN_TO_RHP';
import SEARCH_TO_RHP from './SEARCH_TO_RHP';
import SETTINGS_TO_RHP from './SETTINGS_TO_RHP';
import SIDEBAR_TO_RHP from './SIDEBAR_TO_RHP';
@@ -33,6 +34,7 @@ const SPLIT_TO_SIDEBAR = createInverseRelation(SIDEBAR_TO_SPLIT);
const RHP_TO_WORKSPACES_LIST = createInverseRelation(WORKSPACES_LIST_TO_RHP);
const RHP_TO_SEARCH = createInverseRelation(SEARCH_TO_RHP);
const FULLSCREEN_TO_TAB = createInverseRelation(TAB_TO_FULLSCREEN);
+const RHP_TO_DOMAIN = createInverseRelation(DOMAIN_TO_RHP);
export {
SETTINGS_TO_RHP,
@@ -48,4 +50,5 @@ export {
TAB_TO_FULLSCREEN,
FULLSCREEN_TO_TAB,
RHP_TO_WORKSPACES_LIST,
+ RHP_TO_DOMAIN,
};
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index 94c32b623cfe..4b802c4a5861 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -1081,6 +1081,12 @@ const config: LinkingOptions['config'] = {
[SCREENS.WORKSPACE.PER_DIEM_EDIT_CURRENCY]: {
path: ROUTES.WORKSPACE_PER_DIEM_EDIT_CURRENCY.route,
},
+ [SCREENS.DOMAIN.VERIFY]: {
+ path: ROUTES.DOMAIN_VERIFY.route,
+ },
+ [SCREENS.DOMAIN.VERIFIED]: {
+ path: ROUTES.DOMAIN_VERIFIED.route,
+ },
},
},
[SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: {
@@ -1861,6 +1867,17 @@ const config: LinkingOptions['config'] = {
},
},
+ [NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR]: {
+ screens: {
+ [SCREENS.DOMAIN.INITIAL]: {
+ path: ROUTES.DOMAIN_INITIAL.route,
+ },
+ [SCREENS.DOMAIN.SAML]: {
+ path: ROUTES.DOMAIN_SAML.route,
+ },
+ },
+ },
+
[NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR]: {
screens: {
[SCREENS.SEARCH.ROOT]: {
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 298b054e5ad6..b749c05493ec 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -67,6 +67,7 @@ type SplitNavigatorParamList = {
[NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: SettingsSplitNavigatorParamList;
[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: ReportsSplitNavigatorParamList;
[NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: WorkspaceSplitNavigatorParamList;
+ [NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR]: DomainSplitNavigatorParamList;
};
type SplitNavigatorBySidebar = (typeof SIDEBAR_TO_SPLIT)[T];
@@ -1295,6 +1296,12 @@ type SettingsNavigatorParamList = {
rateID: string;
subRateID: string;
};
+ [SCREENS.DOMAIN.VERIFY]: {
+ accountID: number;
+ };
+ [SCREENS.DOMAIN.VERIFIED]: {
+ accountID: number;
+ };
} & ReimbursementAccountNavigatorParamList;
type DomainCardNavigatorParamList = {
@@ -2339,6 +2346,15 @@ type WorkspaceSplitNavigatorParamList = {
};
};
+type DomainSplitNavigatorParamList = {
+ [SCREENS.DOMAIN.INITIAL]: {
+ accountID: number;
+ };
+ [SCREENS.DOMAIN.SAML]: {
+ accountID: number;
+ };
+};
+
type OnboardingModalNavigatorParamList = {
[SCREENS.ONBOARDING.PERSONAL_DETAILS]: {
// eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md
@@ -2548,6 +2564,7 @@ type AuthScreensParamList = SharedScreensParamList &
[NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: NavigatorScreenParams;
+ [NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.FEATURE_TRAINING_MODAL_NAVIGATOR]: NavigatorScreenParams;
@@ -2726,11 +2743,13 @@ type SearchFullscreenNavigatorName = typeof NAVIGATORS.SEARCH_FULLSCREEN_NAVIGAT
type FullScreenName = SplitNavigatorName | SearchFullscreenNavigatorName | typeof SCREENS.WORKSPACES_LIST;
-// There are two screens/navigators which can be displayed when the Workspaces tab is selected
-type WorkspacesTabNavigatorName = typeof SCREENS.WORKSPACES_LIST | typeof NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR;
+// There are three screens/navigators which can be displayed when the Workspaces tab is selected
+type WorkspacesTabNavigatorName = typeof SCREENS.WORKSPACES_LIST | typeof NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR | typeof NAVIGATORS.DOMAIN_SPLIT_NAVIGATOR;
type WorkspaceScreenName = keyof WorkspaceSplitNavigatorParamList;
+type DomainScreenName = keyof DomainSplitNavigatorParamList;
+
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace ReactNavigation {
@@ -2821,4 +2840,6 @@ export type {
MergeTransactionNavigatorParamList,
AttachmentModalScreensParamList,
WorkspacesDomainModalNavigatorParamList,
+ DomainSplitNavigatorParamList,
+ DomainScreenName,
};
diff --git a/src/pages/domain/DomainVerifiedPage.tsx b/src/pages/domain/BaseDomainVerifiedPage.tsx
similarity index 65%
rename from src/pages/domain/DomainVerifiedPage.tsx
rename to src/pages/domain/BaseDomainVerifiedPage.tsx
index 9c865ff8d569..8db65f395e07 100644
--- a/src/pages/domain/DomainVerifiedPage.tsx
+++ b/src/pages/domain/BaseDomainVerifiedPage.tsx
@@ -11,42 +11,48 @@ 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 {WorkspacesDomainModalNavigatorParamList} from '@libs/Navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import type SCREENS from '@src/SCREENS';
+import type {Route} from '@src/ROUTES';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
-type DomainVerifiedPageProps = PlatformStackScreenProps;
+type BaseDomainVerifiedPageProps = {
+ /** The accountID of the domain */
+ accountID: number;
-function DomainVerifiedPage({route}: DomainVerifiedPageProps) {
+ /** Route to redirect to when trying to access the page for an unverified domain */
+ redirectTo: Route;
+
+ /** Function to run after clicking the confirmation button */
+ navigateAfterConfirmation: () => void;
+};
+
+function BaseDomainVerifiedPage({accountID, redirectTo, navigateAfterConfirmation}: BaseDomainVerifiedPageProps) {
const {translate} = useLocalize();
const styles = useThemeStyles();
- const accountID = route.params.accountID;
const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: false});
-
+ const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
const doesDomainExist = !!domain;
useEffect(() => {
if (!doesDomainExist || domain?.validated) {
return;
}
- Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(accountID), {forceReplace: true}));
- }, [accountID, domain?.validated, doesDomainExist]);
+ Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(redirectTo, {forceReplace: true}));
+ }, [accountID, domain?.validated, doesDomainExist, redirectTo]);
- if (domainMetadata.status === 'loading') {
+ if (isLoadingOnyxValue(domainMetadata, isAdminMetadata)) {
return ;
}
- if (!domain) {
+ if (!domain || !isAdmin) {
return Navigation.dismissModal()} />;
}
return (
@@ -61,11 +67,11 @@ function DomainVerifiedPage({route}: DomainVerifiedPageProps) {
innerContainerStyle={styles.p10}
buttonText={translate('common.buttonConfirm')}
shouldShowButton
- onButtonPress={() => Navigation.dismissModal()}
+ onButtonPress={navigateAfterConfirmation}
/>
);
}
-DomainVerifiedPage.displayName = 'DomainVerifiedPage';
-export default DomainVerifiedPage;
+BaseDomainVerifiedPage.displayName = 'BaseDomainVerifiedPage';
+export default BaseDomainVerifiedPage;
diff --git a/src/pages/domain/VerifyDomainPage.tsx b/src/pages/domain/BaseVerifyDomainPage.tsx
similarity index 88%
rename from src/pages/domain/VerifyDomainPage.tsx
rename to src/pages/domain/BaseVerifyDomainPage.tsx
index 60e01667947d..51ce2e08218e 100644
--- a/src/pages/domain/VerifyDomainPage.tsx
+++ b/src/pages/domain/BaseVerifyDomainPage.tsx
@@ -22,12 +22,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {getDomainValidationCode, resetDomainValidationError, validateDomain} from '@libs/actions/Domain';
import {getLatestErrorMessage} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
-import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
-import type {WorkspacesDomainModalNavigatorParamList} from '@libs/Navigation/types';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import type SCREENS from '@src/SCREENS';
+import type {Route} from '@src/ROUTES';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
function OrderedListRow({index, children}: PropsWithChildren<{index: number}>) {
const styles = useThemeStyles();
@@ -39,26 +37,31 @@ function OrderedListRow({index, children}: PropsWithChildren<{index: number}>) {
);
}
-type VerifyDomainPageProps = PlatformStackScreenProps;
+type BaseVerifyDomainPageProps = {
+ /** The accountID of the domain */
+ accountID: number;
-function VerifyDomainPage({route}: VerifyDomainPageProps) {
+ /** Route to navigate to after successful verification */
+ forwardTo: Route;
+};
+
+function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {translate} = useLocalize();
- const accountID = route.params.accountID;
const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true});
const domainName = domain ? Str.extractEmailDomain(domain.email) : '';
const {isOffline} = useNetwork();
-
+ const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
const doesDomainExist = !!domain;
useEffect(() => {
if (!domain?.validated) {
return;
}
- Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACES_DOMAIN_VERIFIED.getRoute(accountID), {forceReplace: true}));
- }, [accountID, domain?.validated]);
+ Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(forwardTo, {forceReplace: true}));
+ }, [accountID, domain?.validated, forwardTo]);
useEffect(() => {
if (!doesDomainExist) {
@@ -74,17 +77,17 @@ function VerifyDomainPage({route}: VerifyDomainPageProps) {
resetDomainValidationError(accountID);
}, [accountID, doesDomainExist]);
- if (domainMetadata.status === 'loading') {
+ if (isLoadingOnyxValue(domainMetadata, isAdminMetadata)) {
return ;
}
- if (!domain) {
+ if (!domain || !isAdmin) {
return Navigation.dismissModal()} />;
}
return (
@@ -168,5 +171,5 @@ function VerifyDomainPage({route}: VerifyDomainPageProps) {
);
}
-VerifyDomainPage.displayName = 'VerifyDomainPage';
-export default VerifyDomainPage;
+BaseVerifyDomainPage.displayName = 'BaseVerifyDomainPage';
+export default BaseVerifyDomainPage;
diff --git a/src/pages/domain/DomainInitialPage.tsx b/src/pages/domain/DomainInitialPage.tsx
new file mode 100644
index 000000000000..fc3afd8e4b81
--- /dev/null
+++ b/src/pages/domain/DomainInitialPage.tsx
@@ -0,0 +1,128 @@
+import {findFocusedRoute, useNavigationState} from '@react-navigation/native';
+import {Str} from 'expensify-common';
+import React, {useEffect, useMemo} from 'react';
+import {View} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import HighlightableMenuItem from '@components/HighlightableMenuItem';
+import {UserLock} from '@components/Icon/Expensicons';
+import NavigationTabBar from '@components/Navigation/NavigationTabBar';
+import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollView from '@components/ScrollView';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useSingleExecution from '@hooks/useSingleExecution';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWaitForNavigation from '@hooks/useWaitForNavigation';
+import {confirmReadyToOpenApp} from '@libs/actions/App';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type DOMAIN_TO_RHP from '@navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP';
+import type {DomainSplitNavigatorParamList} from '@navigation/types';
+import type CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import type IconAsset from '@src/types/utils/IconAsset';
+
+type DomainTopLevelScreens = keyof typeof DOMAIN_TO_RHP;
+
+type DomainMenuItem = {
+ translationKey: TranslationPaths;
+ icon: IconAsset;
+ action: () => void;
+ brickRoadIndicator?: ValueOf;
+ screenName: DomainTopLevelScreens;
+ badgeText?: string;
+ highlighted?: boolean;
+};
+
+type DomainInitialPageProps = PlatformStackScreenProps;
+
+function DomainInitialPage({route}: DomainInitialPageProps) {
+ const styles = useThemeStyles();
+ const waitForNavigate = useWaitForNavigation();
+ const {singleExecution, isExecuting} = useSingleExecution();
+ const activeRoute = useNavigationState((state) => findFocusedRoute(state)?.name);
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {translate} = useLocalize();
+ const shouldDisplayLHB = !shouldUseNarrowLayout;
+
+ const accountID = route.params?.accountID;
+ const [domain] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true});
+ const domainName = domain ? Str.extractEmailDomain(domain.email) : undefined;
+ const [isAdmin] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
+
+ const domainMenuItems: DomainMenuItem[] = useMemo(() => {
+ const menuItems: DomainMenuItem[] = [
+ {
+ translationKey: 'domain.saml',
+ icon: UserLock,
+ action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.DOMAIN_SAML.getRoute(accountID)))),
+ screenName: SCREENS.DOMAIN.SAML,
+ },
+ ];
+
+ return menuItems;
+ }, [accountID, singleExecution, waitForNavigate]);
+
+ useEffect(() => {
+ confirmReadyToOpenApp();
+ }, []);
+
+ return (
+ }
+ >
+ Navigation.dismissModal()}
+ onLinkPress={Navigation.goBackToHome}
+ shouldShow={!domain || !isAdmin}
+ addBottomSafeAreaPadding
+ shouldForceFullScreen
+ shouldDisplaySearchRouter
+ >
+ Navigation.goBack(ROUTES.WORKSPACES_LIST.route)}
+ shouldDisplayHelpButton={shouldUseNarrowLayout}
+ />
+
+
+
+ {/*
+ Ideally we should use MenuList component for MenuItems with singleExecution/Navigation actions.
+ In this case where user can click on menu items, we need to have a check for `isExecuting`. So, we are directly mapping menuItems.
+ */}
+ {domainMenuItems.map((item) => (
+
+ ))}
+
+
+ {shouldDisplayLHB && }
+
+
+ );
+}
+
+DomainInitialPage.displayName = 'DomainInitialPage';
+
+export default DomainInitialPage;
diff --git a/src/pages/domain/DomainSamlPage.tsx b/src/pages/domain/DomainSamlPage.tsx
new file mode 100644
index 000000000000..81d2c3e34f8c
--- /dev/null
+++ b/src/pages/domain/DomainSamlPage.tsx
@@ -0,0 +1,105 @@
+import {Str} from 'expensify-common';
+import React from 'react';
+import {View} from 'react-native';
+import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
+import type {FeatureListItem} from '@components/FeatureList';
+import FeatureList from '@components/FeatureList';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import {LaptopOnDeskWithCoffeeAndKey, LockClosed, OpenSafe, ShieldYellow} from '@components/Icon/Illustrations';
+import RenderHTML from '@components/RenderHTML';
+import ScreenWrapper from '@components/ScreenWrapper';
+import ScrollViewWithContext from '@components/ScrollViewWithContext';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {DomainSplitNavigatorParamList} from '@libs/Navigation/types';
+import colors from '@styles/theme/colors';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
+
+type DomainSamlPageProps = PlatformStackScreenProps;
+
+const samlFeatures: FeatureListItem[] = [
+ {
+ icon: OpenSafe,
+ translationKey: 'domain.samlFeatureList.fasterAndEasierLogin',
+ },
+ {
+ icon: ShieldYellow,
+ translationKey: 'domain.samlFeatureList.moreSecurityAndControl',
+ },
+ {
+ icon: LockClosed,
+ translationKey: 'domain.samlFeatureList.onePasswordForAnything',
+ },
+];
+
+function DomainSamlPage({route}: DomainSamlPageProps) {
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {translate} = useLocalize();
+
+ const accountID = route.params.accountID;
+ const [domain, domainResults] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true});
+ const [isAdmin, isAdminResults] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false});
+ const domainName = domain ? Str.extractEmailDomain(domain.email) : undefined;
+ const doesDomainExist = !!domain;
+
+ return (
+
+ Navigation.goBack(ROUTES.WORKSPACES_LIST.route)}
+ shouldShow={!isLoadingOnyxValue(domainResults, isAdminResults) && (!doesDomainExist || !isAdmin)}
+ shouldForceFullScreen
+ shouldDisplaySearchRouter
+ >
+
+
+
+
+ (
+
+
+
+ )}
+ ctaText={translate('domain.verifyDomain.title')}
+ ctaAccessibilityLabel={translate('domain.verifyDomain.title')}
+ onCtaPress={() => {
+ Navigation.navigate(ROUTES.DOMAIN_VERIFY.getRoute(accountID));
+ }}
+ illustrationBackgroundColor={colors.blue700}
+ illustration={LaptopOnDeskWithCoffeeAndKey}
+ illustrationStyle={styles.emptyStateSamlIllustration}
+ illustrationContainerStyle={[styles.emptyStateCardIllustrationContainer, styles.justifyContentCenter]}
+ titleStyles={styles.textHeadlineH1}
+ />
+
+
+
+
+ );
+}
+
+DomainSamlPage.displayName = 'DomainSamlPage';
+
+export default DomainSamlPage;
diff --git a/src/pages/domain/SamlDomainVerifiedPage.tsx b/src/pages/domain/SamlDomainVerifiedPage.tsx
new file mode 100644
index 000000000000..25cac7927be9
--- /dev/null
+++ b/src/pages/domain/SamlDomainVerifiedPage.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import BaseDomainVerifiedPage from './BaseDomainVerifiedPage';
+
+type SamlDomainVerifiedPageProps = PlatformStackScreenProps;
+
+function SamlDomainVerifiedPage({route}: SamlDomainVerifiedPageProps) {
+ const accountID = route.params.accountID;
+
+ return (
+ Navigation.navigate(ROUTES.WORKSPACES_LIST.getRoute())}
+ />
+ );
+}
+
+SamlDomainVerifiedPage.displayName = 'SamlDomainVerifiedPage';
+export default SamlDomainVerifiedPage;
diff --git a/src/pages/domain/SamlVerifyDomainPage.tsx b/src/pages/domain/SamlVerifyDomainPage.tsx
new file mode 100644
index 000000000000..efa027338eb5
--- /dev/null
+++ b/src/pages/domain/SamlVerifyDomainPage.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import BaseVerifyDomainPage from './BaseVerifyDomainPage';
+
+type SamlVerifyDomainPageProps = PlatformStackScreenProps;
+
+function SamlVerifyDomainPage({route}: SamlVerifyDomainPageProps) {
+ const accountID = route.params.accountID;
+
+ return (
+
+ );
+}
+
+SamlVerifyDomainPage.displayName = 'SamlVerifyDomainPage';
+export default SamlVerifyDomainPage;
diff --git a/src/pages/domain/WorkspacesDomainVerifiedPage.tsx b/src/pages/domain/WorkspacesDomainVerifiedPage.tsx
new file mode 100644
index 000000000000..6995a1b4f538
--- /dev/null
+++ b/src/pages/domain/WorkspacesDomainVerifiedPage.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import Navigation from '@libs/Navigation/Navigation';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {WorkspacesDomainModalNavigatorParamList} from '@libs/Navigation/types';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import BaseDomainVerifiedPage from './BaseDomainVerifiedPage';
+
+type WorkspacesDomainVerifiedPageProps = PlatformStackScreenProps;
+
+function WorkspacesDomainVerifiedPage({route}: WorkspacesDomainVerifiedPageProps) {
+ const accountID = route.params.accountID;
+
+ return (
+ Navigation.dismissModal()}
+ />
+ );
+}
+
+WorkspacesDomainVerifiedPage.displayName = 'WorkspacesDomainVerifiedPage';
+export default WorkspacesDomainVerifiedPage;
diff --git a/src/pages/domain/WorkspacesVerifyDomainPage.tsx b/src/pages/domain/WorkspacesVerifyDomainPage.tsx
new file mode 100644
index 000000000000..4c595e92ea12
--- /dev/null
+++ b/src/pages/domain/WorkspacesVerifyDomainPage.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
+import type {WorkspacesDomainModalNavigatorParamList} from '@libs/Navigation/types';
+import ROUTES from '@src/ROUTES';
+import type SCREENS from '@src/SCREENS';
+import BaseVerifyDomainPage from './BaseVerifyDomainPage';
+
+type WorkspacesVerifyDomainPageProps = PlatformStackScreenProps;
+
+function WorkspacesVerifyDomainPage({route}: WorkspacesVerifyDomainPageProps) {
+ const accountID = route.params.accountID;
+
+ return (
+
+ );
+}
+
+WorkspacesVerifyDomainPage.displayName = 'WorkspacesVerifyDomainPage';
+export default WorkspacesVerifyDomainPage;
diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx
index 9d422e476452..ac29e2110e72 100755
--- a/src/pages/workspace/WorkspacesListPage.tsx
+++ b/src/pages/workspace/WorkspacesListPage.tsx
@@ -498,7 +498,7 @@ function WorkspacesListPage() {
if (isValidated) {
return openOldDotLink(CONST.OLDDOT_URLS.ADMIN_DOMAINS_URL);
}
- Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(accountID));
+ Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(accountID));
}, []);
/**
diff --git a/src/styles/index.ts b/src/styles/index.ts
index aac1021bd232..25c2318b1d12 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5010,6 +5010,11 @@ const staticStyles = (theme: ThemeColors) =>
...flex.justifyContentCenter,
},
+ emptyStateSamlIllustration: {
+ width: 218,
+ height: 190,
+ },
+
emptyStateCardIllustration: {
width: 164,
height: 190,