diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6e28177dc135f..8eabf9cf27cfc 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1030,7 +1030,7 @@ const CONST = { COLLECT_UPGRADE_HELP_URL: 'https://help.expensify.com/Hidden/collect-upgrade', 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', + DOMAIN_VERIFICATION_HELP_URL: 'https://help.expensify.com/articles/new-expensify/workspaces/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`, diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index ff5685b0cca23..f49d16e6e70bd 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -920,6 +920,8 @@ const ONYXKEYS = { WORKSPACE_PER_DIEM_FORM_DRAFT: 'workspacePerDiemFormDraft', ENABLE_GLOBAL_REIMBURSEMENTS: 'enableGlobalReimbursementsForm', ENABLE_GLOBAL_REIMBURSEMENTS_DRAFT: 'enableGlobalReimbursementsFormDraft', + CREATE_DOMAIN_FORM: 'createDomainForm', + CREATE_DOMAIN_FORM_DRAFT: 'createDomainFormDraft', }, DERIVED: { REPORT_ATTRIBUTES: 'reportAttributes', @@ -1034,6 +1036,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.INTERNATIONAL_BANK_ACCOUNT_FORM]: FormTypes.InternationalBankAccountForm; [ONYXKEYS.FORMS.WORKSPACE_PER_DIEM_FORM]: FormTypes.WorkspacePerDiemForm; [ONYXKEYS.FORMS.ENABLE_GLOBAL_REIMBURSEMENTS]: FormTypes.EnableGlobalReimbursementsForm; + [ONYXKEYS.FORMS.CREATE_DOMAIN_FORM]: FormTypes.CreateDomainForm; }; type OnyxFormDraftValuesMapping = { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9636bc4ef4603..9f3c3e8a53f55 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3394,6 +3394,16 @@ const ROUTES = { route: 'workspaces/domain-verified/:accountID', getRoute: (accountID: number) => `workspaces/domain-verified/${accountID}` as const, }, + WORKSPACES_ADD_DOMAIN: 'workspaces/add-domain', + WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT: `workspaces/add-domain/${VERIFY_ACCOUNT}`, + WORKSPACES_DOMAIN_ADDED: { + route: 'workspaces/domain-added/:accountID', + getRoute: (accountID: number) => `workspaces/domain-added/${accountID}` as const, + }, + WORKSPACES_DOMAIN_ACCESS_RESTRICTED: { + route: 'workspaces/domain-access-restricted/:accountID', + getRoute: (accountID: number) => `workspaces/domain-access-restricted/${accountID}` as const, + }, DOMAIN_INITIAL: { route: 'domain/:accountID', getRoute: (accountID: number) => `domain/${accountID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2909f5951f153..9cfd409288428 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -837,6 +837,10 @@ const SCREENS = { }, WORKSPACES_VERIFY_DOMAIN: 'Workspaces_Verify_Domain', WORKSPACES_DOMAIN_VERIFIED: 'Workspaces_Domain_Verified', + WORKSPACES_ADD_DOMAIN: 'Workspaces_Add_Domain', + WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT: 'Workspaces_Add_Domain_VerifyAccount', + WORKSPACES_DOMAIN_ADDED: 'Workspaces_Domain_Added', + WORKSPACES_DOMAIN_ACCESS_RESTRICTED: 'Workspaces_Domain_Access_Restricted', DOMAIN: { VERIFY: 'Domain_Verify', VERIFIED: 'Domain_Verified', diff --git a/src/components/Domain/DomainMenuItem.tsx b/src/components/Domain/DomainMenuItem.tsx index 1c191445dec19..6f2c438d36f6f 100644 --- a/src/components/Domain/DomainMenuItem.tsx +++ b/src/components/Domain/DomainMenuItem.tsx @@ -75,7 +75,6 @@ function DomainMenuItem({item, index}: DomainMenuItemProps) { accessibilityLabel="row" style={styles.mh5} onPress={action} - disabled={!isAdmin} > {({hovered}) => ( loadIllustration('BlueShield')); + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + Navigation.navigate(ROUTES.WORKSPACES_ADD_DOMAIN)} + /> + } + /> + + ); +} + +DomainsEmptyStateComponent.displayName = 'DomainsEmptyStateComponent'; +export default DomainsEmptyStateComponent; diff --git a/src/languages/de.ts b/src/languages/de.ts index f3acf2ee9ed1b..d3e8117784b0c 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7787,6 +7787,31 @@ ${ fetchError: 'SAML-Konfigurationsdetails konnten nicht abgerufen werden', setMetadataGenericError: 'SAML-Metadaten konnten nicht gesetzt werden', }, + accessRestricted: { + title: 'Zugriff eingeschränkt', + subtitle: (domainName: string) => + `Bitte verifizieren Sie sich als autorisierte/r Unternehmensadministrator/in für ${domainName}, wenn Sie Kontrolle über Folgendes benötigen:`, + companyCardManagement: 'Firmenkartenverwaltung', + accountCreationAndDeletion: 'Kontoerstellung und -löschung', + workspaceCreation: 'Erstellung des Arbeitsbereichs', + samlSSO: 'SAML-SSO', + }, + addDomain: { + title: 'Domain hinzufügen', + subtitle: 'Geben Sie den Namen der privaten Domain ein, auf die Sie zugreifen möchten (z. B. expensify.com).', + domainName: 'Domainname', + newDomain: 'Neue Domain', + }, + domainAdded: { + title: 'Domain hinzugefügt', + description: 'Als Nächstes müssen Sie die Inhaberschaft der Domain bestätigen und Ihre Sicherheitseinstellungen anpassen.', + configure: 'Konfigurieren', + }, + enhancedSecurity: { + title: 'Verbesserte Sicherheit', + subtitle: 'Erzwingen Sie für Mitglieder Ihrer Domain die Anmeldung per Single Sign-On, schränken Sie die Erstellung von Workspaces ein und vieles mehr.', + enable: 'Aktivieren', + }, }, }; // 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 6b4254a38aa0b..922b23463d4fb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7723,6 +7723,30 @@ const translations = { fetchError: "Couldn't fetch SAML configuration details", setMetadataGenericError: "Couldn't set SAML MetaData", }, + accessRestricted: { + title: 'Access restricted', + subtitle: (domainName: string) => `Please verify yourself as an authorized company administrator for ${domainName} if you need control over:`, + companyCardManagement: 'Company card management', + accountCreationAndDeletion: 'Account creation and deletion', + workspaceCreation: 'Workspace creation', + samlSSO: 'SAML SSO', + }, + addDomain: { + title: 'Add domain', + subtitle: 'Enter the name of the private domain you want to access (e.g. expensify.com).', + domainName: 'Domain name', + newDomain: 'New domain', + }, + domainAdded: { + title: 'Domain added', + description: "Next, you'll need to verify ownership of the domain and adjust your security settings.", + configure: 'Configure', + }, + enhancedSecurity: { + title: 'Enhanced security', + subtitle: 'Require members on your domain to log in via single sign-on, restrict workspace creation, and more.', + enable: 'Enable', + }, }, }; diff --git a/src/languages/es.ts b/src/languages/es.ts index c889bdf5a741b..d22f51e207dae 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7842,6 +7842,30 @@ ${amount} para ${merchant} - ${date}`, fetchError: 'No se pudieron obtener los detalles de configuración de SAML', setMetadataGenericError: 'No se pudieron establecer los metadatos de SAML', }, + accessRestricted: { + title: 'Acceso restringido', + subtitle: (domainName: string) => `Por favor, verifícate como un administrador autorizado de la empresa para ${domainName} si necesitas control sobre:`, + companyCardManagement: 'Gestión de tarjetas de la empresa', + accountCreationAndDeletion: 'Creación y eliminación de cuentas', + workspaceCreation: 'Creación de espacios de trabajo', + samlSSO: 'SAML SSO', + }, + addDomain: { + title: 'Añadir dominio', + subtitle: 'Introduce el nombre del dominio privado al que deseas acceder (por ejemplo, expensify.com).', + domainName: 'Nombre de dominio', + newDomain: 'Nuevo dominio', + }, + domainAdded: { + title: 'Dominio añadido', + description: 'A continuación, deberás verificar la propiedad del dominio y ajustar tu configuración de seguridad.', + configure: 'Configurar', + }, + enhancedSecurity: { + title: 'Seguridad mejorada', + subtitle: 'Solicita que los miembros de tu dominio inicien sesión mediante inicio de sesión único, restringe la creación de espacios de trabajo y más.', + enable: 'Habilitar', + }, }, }; diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 3c3f26f987f40..ba4e7c6d97945 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7791,6 +7791,27 @@ ${ fetchError: 'Impossible de récupérer les détails de la configuration SAML', setMetadataGenericError: 'Impossible de définir les métadonnées SAML', }, + accessRestricted: { + title: 'Accès restreint', + subtitle: (domainName: string) => + `Veuillez vous authentifier en tant qu’administrateur d’entreprise autorisé pour ${domainName} si vous avez besoin d’avoir le contrôle sur :`, + companyCardManagement: 'Gestion des cartes d’entreprise', + accountCreationAndDeletion: 'Création et suppression de compte', + workspaceCreation: "Création d'espace de travail", + samlSSO: 'SSO SAML', + }, + addDomain: { + title: 'Ajouter un domaine', + subtitle: 'Saisissez le nom du domaine privé auquel vous souhaitez accéder (par exemple expensify.com).', + domainName: 'Nom de domaine', + newDomain: 'Nouveau domaine', + }, + domainAdded: {title: 'Domaine ajouté', description: 'Ensuite, vous devrez vérifier la propriété du domaine et ajuster vos paramètres de sécurité.', configure: 'Configurer'}, + enhancedSecurity: { + title: 'Sécurité renforcée', + subtitle: "Exiger que les membres de votre domaine se connectent via l'authentification unique, restreindre la création d'espaces de travail, et plus encore.", + enable: 'Activer', + }, }, }; // 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 74a40dcc8a4f0..e92de55be5e29 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7771,6 +7771,30 @@ ${ fetchError: 'Impossibile recuperare i dettagli della configurazione SAML', setMetadataGenericError: 'Impossibile impostare i metadati SAML', }, + accessRestricted: { + title: 'Accesso limitato', + subtitle: (domainName: string) => `Verificati come amministratore aziendale autorizzato per ${domainName} se hai bisogno di gestire:`, + companyCardManagement: 'Gestione delle carte aziendali', + accountCreationAndDeletion: "Creazione e cancellazione dell'account", + workspaceCreation: 'Creazione dello spazio di lavoro', + samlSSO: 'SAML SSO', + }, + addDomain: { + title: 'Aggiungi dominio', + subtitle: 'Inserisci il nome del dominio privato a cui desideri accedere (ad esempio expensify.com).', + domainName: 'Nome di dominio', + newDomain: 'Nuovo dominio', + }, + domainAdded: { + title: 'Dominio aggiunto', + description: 'Successivamente, dovrai verificare la proprietà del dominio e regolare le tue impostazioni di sicurezza.', + configure: 'Configura', + }, + enhancedSecurity: { + title: 'Sicurezza avanzata', + subtitle: 'Richiedi ai membri del tuo dominio di accedere tramite Single Sign-On, limita la creazione di spazi di lavoro e altro ancora.', + enable: 'Abilita', + }, }, }; // 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 42200980e72b8..a3195298fbac7 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7696,6 +7696,26 @@ ${ fetchError: 'SAML 構成の詳細を取得できませんでした', setMetadataGenericError: 'SAML メタデータを設定できませんでした', }, + accessRestricted: { + title: 'アクセスが制限されています', + subtitle: (domainName: string) => `以下を管理する必要がある場合は、${domainName} の認可された会社管理者であることを確認してください:`, + companyCardManagement: '法人カードの管理', + accountCreationAndDeletion: 'アカウントの作成と削除', + workspaceCreation: 'ワークスペースの作成', + samlSSO: 'SAML シングルサインオン', + }, + addDomain: { + title: 'ドメインを追加', + subtitle: 'アクセスしたいプライベートドメイン名を入力してください(例:expensify.com)。', + domainName: 'ドメイン名', + newDomain: '新しいドメイン', + }, + domainAdded: {title: 'ドメインが追加されました', description: '次に、ドメインの所有権を確認し、セキュリティ設定を調整する必要があります。', configure: '設定'}, + enhancedSecurity: { + title: '強化されたセキュリティ', + subtitle: 'ドメインのメンバーにシングルサインオンでのログインを必須化し、ワークスペースの作成を制限するなど、さらに多くのことができます。', + enable: '有効にする', + }, }, }; // 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 37c9ee41296a2..e3287d71ca95d 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7740,6 +7740,30 @@ ${ fetchError: 'Kon SAML-configuratiedetails niet ophalen', setMetadataGenericError: 'Kon SAML-metadata niet instellen', }, + accessRestricted: { + title: 'Toegang beperkt', + subtitle: (domainName: string) => `Bevestig dat u een geautoriseerde bedrijfsbeheerder bent voor ${domainName} als u controle nodig hebt over:`, + companyCardManagement: 'Beheer van bedrijfskaarten', + accountCreationAndDeletion: 'Accountaanmaak en -verwijdering', + workspaceCreation: 'Werkruimte aanmaken', + samlSSO: 'SAML SSO', + }, + addDomain: { + title: 'Domein toevoegen', + subtitle: 'Voer de naam in van het privédomein waartoe je toegang wilt krijgen (bijv. expensify.com).', + domainName: 'Domeinnaam', + newDomain: 'Nieuw domein', + }, + domainAdded: { + title: 'Domein toegevoegd', + description: 'Vervolgens moet je het eigendom van het domein verifiëren en je beveiligingsinstellingen aanpassen.', + configure: 'Configureren', + }, + enhancedSecurity: { + title: 'Verbeterde beveiliging', + subtitle: 'Verplicht leden van je domein om in te loggen via single sign-on, beperk het aanmaken van werkruimten en meer.', + enable: 'Inschakelen', + }, }, }; // 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 aef6ee6ae6e4d..b9c03b618066f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7731,6 +7731,26 @@ ${ fetchError: 'Nie udało się pobrać szczegółów konfiguracji SAML', setMetadataGenericError: 'Nie można ustawić metadanych SAML', }, + accessRestricted: { + title: 'Dostęp ograniczony', + subtitle: (domainName: string) => `Proszę zweryfikować się jako autoryzowany administrator firmy dla ${domainName}, jeśli potrzebujesz kontroli nad:`, + companyCardManagement: 'Zarządzanie kartami firmowymi', + accountCreationAndDeletion: 'Tworzenie i usuwanie konta', + workspaceCreation: 'Tworzenie obszaru roboczego', + samlSSO: 'SAML SSO', + }, + addDomain: { + title: 'Dodaj domenę', + subtitle: 'Wprowadź nazwę prywatnej domeny, do której chcesz uzyskać dostęp (np. expensify.com).', + domainName: 'Nazwa domeny', + newDomain: 'Nowa domena', + }, + domainAdded: {title: 'Dodano domenę', description: 'Następnie musisz zweryfikować własność domeny i dostosować ustawienia zabezpieczeń.', configure: 'Skonfiguruj'}, + enhancedSecurity: { + title: 'Zwiększone bezpieczeństwo', + subtitle: 'Wymagaj, aby członkowie Twojej domeny logowali się przez Single Sign-On (SSO), ograniczaj tworzenie obszarów roboczych i nie tylko.', + enable: 'Włącz', + }, }, }; // 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 5000e850b098d..fa72769bb09ef 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7747,6 +7747,30 @@ ${ fetchError: 'Não foi possível obter os detalhes da configuração SAML', setMetadataGenericError: 'Não foi possível definir os metadados SAML', }, + accessRestricted: { + title: 'Acesso restrito', + subtitle: (domainName: string) => `Confirme que você é um administrador autorizado da empresa para ${domainName} se precisar de controle sobre:`, + companyCardManagement: 'Gerenciamento de cartões corporativos', + accountCreationAndDeletion: 'Criação e exclusão de conta', + workspaceCreation: 'Criação do espaço de trabalho', + samlSSO: 'SSO SAML', + }, + addDomain: { + title: 'Adicionar domínio', + subtitle: 'Digite o nome do domínio privado que você deseja acessar (ex.: expensify.com).', + domainName: 'Nome de domínio', + newDomain: 'Novo domínio', + }, + domainAdded: { + title: 'Domínio adicionado', + description: 'Em seguida, você precisará verificar a propriedade do domínio e ajustar suas configurações de segurança.', + configure: 'Configurar', + }, + enhancedSecurity: { + title: 'Segurança aprimorada', + subtitle: 'Exija que os membros do seu domínio façam login por meio de logon único (SSO), restrinja a criação de espaços de trabalho e muito mais.', + enable: 'Ativar', + }, }, }; // 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 a7490c686940b..60511c5b9ad6a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7574,6 +7574,17 @@ ${ fetchError: '无法获取 SAML 配置详细信息', setMetadataGenericError: '无法设置 SAML 元数据', }, + accessRestricted: { + title: '访问受限', + subtitle: (domainName: string) => `如果您需要对以下内容进行管理,请验证您是 ${domainName} 的授权公司管理员:`, + companyCardManagement: '公司卡管理', + accountCreationAndDeletion: '账户创建和删除', + workspaceCreation: '工作区创建', + samlSSO: 'SAML 单点登录', + }, + addDomain: {title: '添加域', subtitle: '请输入您想访问的私有域名(例如:expensify.com)。', domainName: '域名', newDomain: '新域名'}, + domainAdded: {title: '已添加域名', description: '接下来,您需要验证域名的所有权并调整您的安全设置。', configure: '配置'}, + enhancedSecurity: {title: '增强的安全性', subtitle: '要求您域内的成员使用单点登录登录、限制工作区创建等。', enable: '启用'}, }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 6c2a626e111b0..0294f886b5c21 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -523,6 +523,7 @@ const WRITE_COMMANDS = { SET_SAML_IDENTITY: 'SetSAMLIdentity', UPDATE_SAML_ENABLED: 'UpdateSAMLEnabled', UPDATE_SAML_REQUIRED: 'UpdateSAMLRequired', + CREATE_DOMAIN: 'CreateDomain', } as const; type WriteCommand = ValueOf; @@ -1067,6 +1068,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_SAML_IDENTITY]: Parameters.SetSamlIdentityParams; [WRITE_COMMANDS.UPDATE_SAML_ENABLED]: Parameters.UpdateSamlEnabledParams; [WRITE_COMMANDS.UPDATE_SAML_REQUIRED]: Parameters.UpdateSamlRequiredParams; + [WRITE_COMMANDS.CREATE_DOMAIN]: Parameters.DomainParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 3ba91b84fbc95..0c59115ab6a52 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1006,6 +1006,10 @@ const ScheduleCallModalStackNavigator = createModalStackNavigator({ [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/WorkspacesVerifyDomainPage').default, [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/WorkspacesDomainVerifiedPage').default, + [SCREENS.WORKSPACES_ADD_DOMAIN]: () => require('../../../../pages/domain/AddDomainPage').default, + [SCREENS.WORKSPACES_DOMAIN_ADDED]: () => require('../../../../pages/domain/DomainAddedPage').default, + [SCREENS.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT]: () => require('../../../../pages/domain/AddDomainVerifyAccountPage').default, + [SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED]: () => require('../../../../pages/domain/DomainAccessRestrictedPage').default, }); export { diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts index 40eebe61d4be7..a40fab3fc8194 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts @@ -1,7 +1,16 @@ import SCREENS from '@src/SCREENS'; const WORKSPACES_LIST_TO_RHP: Record = { - [SCREENS.WORKSPACES_LIST]: [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES, SCREENS.WORKSPACE_DUPLICATE.ROOT, SCREENS.WORKSPACES_VERIFY_DOMAIN, SCREENS.WORKSPACES_DOMAIN_VERIFIED], + [SCREENS.WORKSPACES_LIST]: [ + SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES, + SCREENS.WORKSPACE_DUPLICATE.ROOT, + SCREENS.WORKSPACES_VERIFY_DOMAIN, + SCREENS.WORKSPACES_DOMAIN_VERIFIED, + SCREENS.WORKSPACES_ADD_DOMAIN, + SCREENS.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT, + SCREENS.WORKSPACES_DOMAIN_ADDED, + SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED, + ], }; export default WORKSPACES_LIST_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index e89a203039027..1fe15e1da5d0e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1773,6 +1773,16 @@ const config: LinkingOptions['config'] = { path: ROUTES.WORKSPACES_DOMAIN_VERIFIED.route, exact: true, }, + [SCREENS.WORKSPACES_ADD_DOMAIN]: ROUTES.WORKSPACES_ADD_DOMAIN, + [SCREENS.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT]: ROUTES.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT, + [SCREENS.WORKSPACES_DOMAIN_ADDED]: { + path: ROUTES.WORKSPACES_DOMAIN_ADDED.route, + exact: true, + }, + [SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED]: { + path: ROUTES.WORKSPACES_DOMAIN_ACCESS_RESTRICTED.route, + exact: true, + }, }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index ba4a686fb19e0..4cab857c9c575 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2086,6 +2086,14 @@ type WorkspacesDomainModalNavigatorParamList = { [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: { accountID: number; }; + [SCREENS.WORKSPACES_ADD_DOMAIN]: undefined; + [SCREENS.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT]: undefined; + [SCREENS.WORKSPACES_DOMAIN_ADDED]: { + accountID: number; + }; + [SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED]: { + accountID: number; + }; }; type RightModalNavigatorParamList = { diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts index 1ae75a17d01fc..70401cbe524f7 100644 --- a/src/libs/actions/Domain.ts +++ b/src/libs/actions/Domain.ts @@ -49,7 +49,7 @@ function validateDomain(accountID: number, domainName: string) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, - value: {isValidationPending: true, domainValidationError: null}, + value: {isValidationPending: true, domainValidationError: null, hasValidationSucceeded: null}, }, ]; @@ -57,7 +57,7 @@ function validateDomain(accountID: number, domainName: string) { { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, - value: {isValidationPending: null}, + value: {isValidationPending: null, hasValidationSucceeded: true}, }, ]; @@ -304,6 +304,41 @@ async function getScimToken(domainName: string): Promise { } } +/** Sends request for claiming a domain */ +function createDomain(domainName: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CREATE_DOMAIN_FORM, + value: {hasCreationSucceeded: null, isLoading: true}, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CREATE_DOMAIN_FORM, + value: {hasCreationSucceeded: true, isLoading: null}, + }, + ]; + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.FORMS.CREATE_DOMAIN_FORM, + value: {isLoading: null}, + }, + ]; + + API.write(WRITE_COMMANDS.CREATE_DOMAIN, {domainName}, {optimisticData, successData, failureData}); +} + +/** + * For resetting createDomain form data + * Resets it only on the client's side, no server call is performed + */ +function resetCreateDomainForm() { + Onyx.merge(ONYXKEYS.FORMS.CREATE_DOMAIN_FORM, null); +} + export { getDomainValidationCode, validateDomain, @@ -316,4 +351,6 @@ export { resetSamlRequiredError, setSamlIdentity, getScimToken, + createDomain, + resetCreateDomainForm, }; diff --git a/src/pages/domain/AddDomainPage.tsx b/src/pages/domain/AddDomainPage.tsx new file mode 100644 index 0000000000000..2fa84791e2014 --- /dev/null +++ b/src/pages/domain/AddDomainPage.tsx @@ -0,0 +1,114 @@ +import {Str} from 'expensify-common'; +import React, {useCallback, useEffect, useRef} from 'react'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {createDomain, resetCreateDomainForm} from '@libs/actions/Domain'; +import {clearDraftValues} from '@libs/actions/FormActions'; +import Navigation from '@libs/Navigation/Navigation'; +import {getFieldRequiredErrors, isPublicDomain} from '@libs/ValidationUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import {isUserValidatedSelector} from '@src/selectors/Account'; +import INPUT_IDS from '@src/types/form/CreateDomainForm'; + +function AddDomainPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + + const [form] = useOnyx(ONYXKEYS.FORMS.CREATE_DOMAIN_FORM, {canBeMissing: true}); + const [allDomains] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN, {canBeMissing: false}); + const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {canBeMissing: true, selector: isUserValidatedSelector}); + + const validate = useCallback( + (values: FormOnyxValues) => { + const errors = getFieldRequiredErrors(values, [INPUT_IDS.DOMAIN_NAME]); + const domainName = values[INPUT_IDS.DOMAIN_NAME]; + if (!domainName) { + return errors; + } + if (!Str.isValidDomainName(domainName)) { + errors[INPUT_IDS.DOMAIN_NAME] = translate('iou.invalidDomainError'); + } else if (isPublicDomain(domainName)) { + errors[INPUT_IDS.DOMAIN_NAME] = translate('iou.publicDomainError'); + } + return errors; + }, + [translate], + ); + + const submittedDomainName = useRef(undefined); + + useEffect(() => { + if (!form?.hasCreationSucceeded) { + return; + } + + // Find the newly created domain because the accountID is not optimistically created in App, but created in BE + const accountID = Object.values(allDomains ?? {})?.find( + (domain) => domain && submittedDomainName.current && Str.caseInsensitiveEquals(Str.extractEmailDomain(domain.email), submittedDomainName.current), + )?.accountID; + if (accountID) { + Navigation.navigate(ROUTES.WORKSPACES_DOMAIN_ADDED.getRoute(accountID), {forceReplace: true}); + } + }, [form?.hasCreationSucceeded, allDomains]); + + useEffect(() => { + resetCreateDomainForm(); + return () => clearDraftValues(ONYXKEYS.FORMS.CREATE_DOMAIN_FORM); + }, []); + + return ( + + Navigation.goBack(ROUTES.WORKSPACES_LIST.getRoute())} + /> + + {translate('domain.addDomain.subtitle')} + + { + if (!isUserValidated) { + return Navigation.navigate(ROUTES.WORKSPACES_ADD_DOMAIN_VERIFY_ACCOUNT); + } + submittedDomainName.current = domainName; + createDomain(domainName); + }} + isLoading={form?.isLoading} + > + + + + + ); +} + +AddDomainPage.displayName = 'AddDomainPage'; +export default AddDomainPage; diff --git a/src/pages/domain/AddDomainVerifyAccountPage.tsx b/src/pages/domain/AddDomainVerifyAccountPage.tsx new file mode 100644 index 0000000000000..da30a563096a1 --- /dev/null +++ b/src/pages/domain/AddDomainVerifyAccountPage.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import VerifyAccountPageBase from '@pages/settings/VerifyAccountPageBase'; +import ROUTES from '@src/ROUTES'; + +function AddDomainVerifyAccountPage() { + return ( + + ); +} + +AddDomainVerifyAccountPage.displayName = 'AddDomainVerifyAccountPage'; +export default AddDomainVerifyAccountPage; diff --git a/src/pages/domain/BaseDomainVerifiedPage.tsx b/src/pages/domain/BaseDomainVerifiedPage.tsx index 6482fc5e0a3ab..c88f4c9f7a6c6 100644 --- a/src/pages/domain/BaseDomainVerifiedPage.tsx +++ b/src/pages/domain/BaseDomainVerifiedPage.tsx @@ -14,6 +14,7 @@ import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; +import ROUTES from '@src/ROUTES'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; type BaseDomainVerifiedPageProps = { @@ -64,7 +65,7 @@ function BaseDomainVerifiedPage({accountID, redirectTo}: BaseDomainVerifiedPageP innerContainerStyle={styles.p10} buttonText={translate('common.buttonConfirm')} shouldShowButton - onButtonPress={() => Navigation.dismissModal()} + onButtonPress={() => Navigation.navigate(ROUTES.DOMAIN_INITIAL.getRoute(accountID))} /> ); diff --git a/src/pages/domain/BaseVerifyDomainPage.tsx b/src/pages/domain/BaseVerifyDomainPage.tsx index 5dfb3d9576d87..e073f23a9d8c9 100644 --- a/src/pages/domain/BaseVerifyDomainPage.tsx +++ b/src/pages/domain/BaseVerifyDomainPage.tsx @@ -51,17 +51,16 @@ function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps) const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: true}); const domainName = domain ? Str.extractEmailDomain(domain.email) : ''; - const [isAdmin, isAdminMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_ADMIN_ACCESS}${accountID}`, {canBeMissing: false}); const doesDomainExist = !!domain; const {asset: Exclamation} = useMemoizedLazyAsset(() => loadExpensifyIcon('Exclamation')); useEffect(() => { - if (!domain?.validated) { + if (!domain?.hasValidationSucceeded) { return; } Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(forwardTo, {forceReplace: true})); - }, [accountID, domain?.validated, forwardTo]); + }, [accountID, domain?.hasValidationSucceeded, forwardTo]); useEffect(() => { if (!doesDomainExist) { @@ -77,11 +76,11 @@ function BaseVerifyDomainPage({accountID, forwardTo}: BaseVerifyDomainPageProps) resetDomainValidationError(accountID); }, [accountID, doesDomainExist]); - if (isLoadingOnyxValue(domainMetadata, isAdminMetadata)) { + if (isLoadingOnyxValue(domainMetadata)) { return ; } - if (!domain || !isAdmin) { + if (!domain) { return Navigation.dismissModal()} />; } diff --git a/src/pages/domain/DomainAccessRestrictedPage.tsx b/src/pages/domain/DomainAccessRestrictedPage.tsx new file mode 100644 index 0000000000000..928c602638e4d --- /dev/null +++ b/src/pages/domain/DomainAccessRestrictedPage.tsx @@ -0,0 +1,98 @@ +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import FixedFooter from '@components/FixedFooter'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import {loadExpensifyIcon} from '@components/Icon/ExpensifyIconLoader'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import {useMemoizedLazyAsset} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useTheme from '@hooks/useTheme'; +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 type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import {domainNameSelector} from '@src/selectors/Domain'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; + +type DomainAccessRestrictedPageProps = PlatformStackScreenProps; + +const FEATURES: TranslationPaths[] = [ + 'domain.accessRestricted.companyCardManagement', + 'domain.accessRestricted.accountCreationAndDeletion', + 'domain.accessRestricted.workspaceCreation', + 'domain.accessRestricted.samlSSO', +]; + +function DomainAccessRestrictedPage({route}: DomainAccessRestrictedPageProps) { + const {asset: Checkmark} = useMemoizedLazyAsset(() => loadExpensifyIcon('Checkmark')); + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + const accountID = route.params.accountID; + const [domainName, domainNameResults] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {canBeMissing: false, selector: domainNameSelector}); + + if (isLoadingOnyxValue(domainNameResults)) { + return ; + } + + if (!domainName) { + return Navigation.dismissModal()} />; + } + + return ( + + + + + + + + + {FEATURES.map((featureTranslationPath) => ( + + + {translate(featureTranslationPath)} + + ))} + + + +