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,