diff --git a/cspell.json b/cspell.json index bbbff86b5fdb0..af24e776c01d2 100644 --- a/cspell.json +++ b/cspell.json @@ -779,7 +779,8 @@ "Selec", "setuptools", "DYNAMICEXTERNAL", - "RNCORE" + "RNCORE", + "Wooo" ], "ignorePaths": [ "src/languages/de.ts", diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 061db10e8dd7a..4730740703160 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1026,6 +1026,7 @@ const CONST = { PLAN_TYPES_AND_PRICING_HELP_URL: 'https://help.expensify.com/articles/new-expensify/billing-and-subscriptions/Plan-types-and-pricing', 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', 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 diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 36cdd042c048d..3b192bd39c728 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -3331,6 +3331,14 @@ const ROUTES = { // eslint-disable-next-line no-restricted-syntax -- Legacy route generation getRoute: (backTo?: string) => getUrlWithBackToParam('test-tools' as const, backTo), }, + WORKSPACES_VERIFY_DOMAIN: { + route: 'workspaces/verify-domain/:accountID', + getRoute: (accountID: number) => `workspaces/verify-domain/${accountID}` as const, + }, + WORKSPACES_DOMAIN_VERIFIED: { + route: 'workspaces/domain-verified/:accountID', + getRoute: (accountID: number) => `workspaces/domain-verified/${accountID}` as const, + }, } as const; /** diff --git a/src/SCREENS.ts b/src/SCREENS.ts index b07684406f658..acbdd26627cba 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -258,6 +258,7 @@ const SCREENS = { REPORT_CHANGE_APPROVER: 'Report_Change_Approver', REPORT_VERIFY_ACCOUNT: 'Report_Verify_Account', MERGE_TRANSACTION: 'MergeTransaction', + DOMAIN: 'Domain', }, PUBLIC_CONSOLE_DEBUG: 'Console_Debug', SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', @@ -818,6 +819,8 @@ const SCREENS = { TEST_TOOLS_MODAL: { ROOT: 'TestToolsModal_Root', }, + WORKSPACES_VERIFY_DOMAIN: 'Workspaces_Verify_Domain', + WORKSPACES_DOMAIN_VERIFIED: 'Workspaces_Domain_Verified', } as const; type Screen = DeepValueOf; diff --git a/src/components/Domain/CopyableTextField.tsx b/src/components/Domain/CopyableTextField.tsx new file mode 100644 index 0000000000000..8310b74bb5e13 --- /dev/null +++ b/src/components/Domain/CopyableTextField.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import {View} from 'react-native'; +import ActivityIndicator from '@components/ActivityIndicator'; +import CopyTextToClipboard from '@components/CopyTextToClipboard'; +import Text from '@components/Text'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +type CopyableTextFieldProps = { + /** Text to display and to copy */ + value?: string; + + /** Should an activity indicator be shown instead of the text and button */ + isLoading?: boolean; +}; + +function CopyableTextField({value, isLoading = false}: CopyableTextFieldProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + return ( + + {isLoading ? ( + + ) : ( + <> + {value ?? ''} + + + + + )} + + ); +} + +CopyableTextField.displayName = 'CopyableTextField'; +export default CopyableTextField; diff --git a/src/components/Domain/DomainMenuItem.tsx b/src/components/Domain/DomainMenuItem.tsx new file mode 100644 index 0000000000000..7bdef86bbe058 --- /dev/null +++ b/src/components/Domain/DomainMenuItem.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +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 useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import DomainsListRow from './DomainsListRow'; + +type DomainMenuItemProps = { + /** Domain menu item data */ + item: DomainItem; + + /** Row index in the menu */ + index: number; +}; + +type DomainItem = { + /** Type of menu item row in the list of workspaces and domains */ + listItemType: 'domain'; + + /** Main text to show in the row */ + title: string; + + /** Function to run after clicking on the row */ + action: () => void; + + /** ID of the row's domain */ + accountID: number; + + /** Whether the user is an admin of the row's domain */ + isAdmin: boolean; + + /** Whether the row's domain is validated (aka verified) */ + isValidated: boolean; +} & Pick; + +function DomainMenuItem({item, index}: DomainMenuItemProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {isAdmin, isValidated} = item; + + const threeDotsMenuItems: PopoverMenuItem[] | undefined = + !isValidated && isAdmin + ? [ + { + icon: Expensicons.Globe, + text: translate('domain.verifyDomain.title'), + onSelected: () => Navigation.navigate(ROUTES.WORKSPACES_VERIFY_DOMAIN.getRoute(item.accountID)), + }, + ] + : undefined; + + return ( + + + {({hovered}) => ( + + )} + + + ); +} + +DomainMenuItem.displayName = 'DomainMenuItem'; + +export type {DomainItem}; +export default DomainMenuItem; diff --git a/src/components/Domain/DomainsListRow.tsx b/src/components/Domain/DomainsListRow.tsx index 79cecbc180f9c..adfe7d9b2377f 100644 --- a/src/components/Domain/DomainsListRow.tsx +++ b/src/components/Domain/DomainsListRow.tsx @@ -1,10 +1,15 @@ import React from 'react'; import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import Badge from '@components/Badge'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import TextWithTooltip from '@components/TextWithTooltip'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; type DomainsListRowProps = { /** Name of the domain */ @@ -13,17 +18,23 @@ type DomainsListRowProps = { /** Whether the row is hovered, so we can modify its style */ isHovered: boolean; - /** Whether the icon at the end of the row should be displayed */ - shouldShowRightIcon: boolean; + /** The text to display inside a badge next to the title */ + badgeText?: string; + + /** Items for the three dots menu */ + menuItems?: PopoverMenuItem[]; + + /** The type of brick road indicator to show. */ + brickRoadIndicator?: ValueOf; }; -function DomainsListRow({title, isHovered, shouldShowRightIcon}: DomainsListRowProps) { +function DomainsListRow({title, isHovered, badgeText, brickRoadIndicator, menuItems}: DomainsListRowProps) { const styles = useThemeStyles(); const theme = useTheme(); return ( - - + + + + {!!badgeText && ( + + + + )} - {shouldShowRightIcon && ( + + + + + {!!brickRoadIndicator && ( + + )} + + {!!menuItems?.length && ( + + )} + + - )} + ); } diff --git a/src/components/WorkspacesEmptyStateComponent.tsx b/src/components/WorkspacesEmptyStateComponent.tsx new file mode 100644 index 0000000000000..a20cb77e22717 --- /dev/null +++ b/src/components/WorkspacesEmptyStateComponent.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import useLocalize from '@hooks/useLocalize'; +import usePreferredPolicy from '@hooks/usePreferredPolicy'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; +import Navigation from '@libs/Navigation/Navigation'; +import colors from '@styles/theme/colors'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import EmptyStateComponent from './EmptyStateComponent'; +import LottieAnimations from './LottieAnimations'; +import WorkspaceRowSkeleton from './Skeletons/WorkspaceRowSkeleton'; + +function WorkspacesEmptyStateComponent() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const StyleUtils = useStyleUtils(); + const {isRestrictedPolicyCreation} = usePreferredPolicy(); + + return ( + interceptAnonymousUser(() => Navigation.navigate(ROUTES.WORKSPACE_CONFIRMATION.getRoute(ROUTES.WORKSPACES_LIST.route))), + buttonText: translate('workspace.new.newWorkspace'), + }, + ] + } + /> + ); +} + +WorkspacesEmptyStateComponent.displayName = 'WorkspacesEmptyStateComponent'; +export default WorkspacesEmptyStateComponent; diff --git a/src/languages/de.ts b/src/languages/de.ts index fa55bc87d78b2..3b3e04f90aa99 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7409,6 +7409,28 @@ ${amount} für ${merchant} - ${date}`, subtitle: `Wir konnten nicht alle Ihre Daten laden. Wir wurden benachrichtigt und untersuchen das Problem. Wenn das weiterhin besteht, wenden Sie sich bitte an`, refreshAndTryAgain: 'Aktualisieren und erneut versuchen', }, + domain: { + notVerified: 'Nicht verifiziert', + retry: 'Erneut versuchen', + verifyDomain: { + title: 'Domain verifizieren', + beforeProceeding: ({domainName}: {domainName: string}) => + `Bevor Sie fortfahren, bestätigen Sie, dass Sie ${domainName} besitzen, indem Sie die DNS-Einstellungen der Domain aktualisieren.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Greifen Sie auf Ihren DNS-Anbieter zu und öffnen Sie die DNS-Einstellungen für ${domainName}.`, + addTXTRecord: 'Fügen Sie den folgenden TXT-Eintrag hinzu:', + saveChanges: 'Speichern Sie die Änderungen und kehren Sie hierher zurück, um Ihre Domain zu verifizieren.', + youMayNeedToConsult: `Möglicherweise müssen Sie sich an die IT-Abteilung Ihrer Organisation wenden, um die Verifizierung abzuschließen. Weitere Informationen.`, + warning: 'Nach der Verifizierung erhalten alle Expensify-Mitglieder in Ihrer Domain eine E-Mail, dass ihr Konto unter Ihrer Domain verwaltet wird.', + codeFetchError: 'Verifizierungscode konnte nicht abgerufen werden', + genericError: 'Wir konnten Ihre Domain nicht verifizieren. Bitte versuchen Sie es erneut und wenden Sie sich an Concierge, wenn das Problem weiterhin besteht.', + }, + domainVerified: { + title: 'Domain verifiziert', + header: 'Wooo! Ihre Domain wurde verifiziert', + description: ({domainName}: {domainName: string}) => + `Die Domain ${domainName} wurde erfolgreich verifiziert und Sie können jetzt SAML und andere Sicherheitsfunktionen einrichten.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/en.ts b/src/languages/en.ts index 58bced4bb71ba..813227348728a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7340,6 +7340,27 @@ const translations = { exportInProgress: 'Export in progress', conciergeWillSend: 'Concierge will send you the file shortly.', }, + domain: { + notVerified: 'Not verified', + retry: 'Retry', + verifyDomain: { + title: 'Verify domain', + beforeProceeding: ({domainName}: {domainName: string}) => `Before proceeding, verify that you own ${domainName} by updating its DNS settings.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Access your DNS provider and open DNS settings for ${domainName}.`, + addTXTRecord: 'Add the following TXT record:', + saveChanges: 'Save changes and return here to verify your domain.', + youMayNeedToConsult: `You may need to consult your organization's IT department to complete verification. Learn more.`, + warning: 'After verification, all Expensify members on your domain will receive an email that their account will be managed under your domain.', + codeFetchError: 'Couldn’t fetch verification code', + genericError: "We couldn't verify your domain. Please try again and reach out to Concierge if the problem persists.", + }, + domainVerified: { + title: 'Domain verified', + header: 'Wooo! Your domain has been verified', + description: ({domainName}: {domainName: string}) => + `The domain ${domainName} has been successfully verified and you can now set up SAML and other security features.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, diff --git a/src/languages/es.ts b/src/languages/es.ts index df6bda3d8403d..d1ee7628819eb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7845,6 +7845,29 @@ ${amount} para ${merchant} - ${date}`, subtitle: `No hemos podido cargar todos sus datos. Hemos sido notificados y estamos investigando el problema. Si esto persiste, por favor comuníquese con`, refreshAndTryAgain: 'Actualizar e intentar de nuevo', }, + domain: { + notVerified: 'No verificado', + retry: 'Reintentar', + verifyDomain: { + title: 'Verificar dominio', + beforeProceeding: ({domainName}: {domainName: string}) => + `Antes de continuar, verifica que eres propietario de ${domainName} actualizando su configuración DNS.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Accede a tu proveedor de DNS y abre la configuración DNS de ${domainName}.`, + addTXTRecord: 'Añade el siguiente registro TXT:', + saveChanges: 'Guarda los cambios y vuelve aquí para verificar tu dominio.', + youMayNeedToConsult: `Es posible que necesites consultar con el servicio informático de tu organización para completar la verificación. Más información.`, + warning: + 'Después de la verificación, todos los miembros de Expensify en tu dominio recibirán un correo electrónico informando que sus cuentas serán gestionadas bajo tu dominio.', + codeFetchError: 'No se pudo obtener el código de verificación', + genericError: 'No pudimos verificar tu dominio. Por favor, inténtalo de nuevo y contacta con Concierge si el problema persiste.', + }, + domainVerified: { + title: 'Dominio verificado', + header: '¡Wooo! Tu dominio ha sido verificado', + description: ({domainName}: {domainName: string}) => + `El dominio ${domainName} se ha verificado correctamente y ahora puedes configurar SAML y otras funciones de seguridad.`, + }, + }, }; export default translations satisfies TranslationDeepObject; diff --git a/src/languages/fr.ts b/src/languages/fr.ts index ded7be6512ab4..97a12baf1f143 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7409,6 +7409,28 @@ ${amount} pour ${merchant} - ${date}`, subtitle: `Nous n'avons pas pu charger toutes vos données. Nous avons été informés et examinons le problème. Si cela persiste, veuillez contacter`, refreshAndTryAgain: 'Actualisez puis réessayez', }, + domain: { + notVerified: 'Non vérifié', + retry: 'Réessayer', + verifyDomain: { + title: 'Vérifier le domaine', + beforeProceeding: ({domainName}: {domainName: string}) => + `Avant de poursuivre, vérifiez que vous êtes propriétaire de ${domainName} en mettant à jour ses paramètres DNS.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Accédez à votre fournisseur DNS et ouvrez les paramètres DNS pour ${domainName}.`, + addTXTRecord: 'Ajoutez l’enregistrement TXT suivant :', + saveChanges: 'Enregistrez les modifications et revenez ici pour vérifier votre domaine.', + youMayNeedToConsult: `Il se peut que vous deviez consulter le service informatique de votre organisation pour terminer la vérification. En savoir plus.`, + warning: 'Après vérification, tous les membres Expensify de votre domaine recevront un e-mail indiquant que leur compte sera géré au sein de votre domaine.', + codeFetchError: 'Impossible de récupérer le code de vérification', + genericError: "Nous n'avons pas pu vérifier votre domaine. Veuillez réessayer et contacter Concierge si le problème persiste.", + }, + domainVerified: { + title: 'Domaine vérifié', + header: 'Wouhou ! Votre domaine a été vérifié', + 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é.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/it.ts b/src/languages/it.ts index 4d02de1932b4f..91f6a14302cf9 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7416,6 +7416,28 @@ ${amount} per ${merchant} - ${date}`, subtitle: `Non siamo riusciti a caricare tutti i tuoi dati. Siamo stati avvisati e stiamo esaminando il problema. Se il problema persiste, contatta`, refreshAndTryAgain: 'Aggiorna e riprova', }, + domain: { + notVerified: 'Non verificato', + retry: 'Riprova', + verifyDomain: { + title: 'Verifica dominio', + beforeProceeding: ({domainName}: {domainName: string}) => + `Prima di procedere, verifica di essere il proprietario di ${domainName} aggiornando le impostazioni DNS.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Accedi al tuo provider DNS e apri le impostazioni DNS per ${domainName}.`, + addTXTRecord: 'Aggiungi il seguente record TXT:', + saveChanges: 'Salva le modifiche e torna qui per verificare il tuo dominio.', + youMayNeedToConsult: `Potresti dover contattare il reparto IT della tua organizzazione per completare la verifica. Scopri di più.`, + warning: "Dopo la verifica, tutti i membri di Expensify del tuo dominio riceveranno un'email che li informa che il loro account sarà gestito all'interno del tuo dominio.", + codeFetchError: 'Impossibile recuperare il codice di verifica', + genericError: 'Non siamo riusciti a verificare il tuo dominio. Riprova e contatta Concierge se il problema persiste.', + }, + domainVerified: { + title: 'Dominio verificato', + header: 'Wooo! Il tuo dominio è stato verificato', + description: ({domainName}: {domainName: string}) => + `Il dominio ${domainName} è stato verificato con successo e ora puoi configurare SAML e altre funzionalità di sicurezza.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 5f6d09c5cdaf4..f618c3f5d792c 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7341,6 +7341,27 @@ ${date} - ${merchant}に${amount}`, subtitle: `すべてのデータを読み込むことができませんでした。通知を受けており、問題を調査しています。この状態が続く場合は、お問い合わせください。`, refreshAndTryAgain: '再読み込みして、もう一度お試しください', }, + domain: { + notVerified: '未確認', + retry: '再試行', + verifyDomain: { + title: 'ドメインを確認', + beforeProceeding: ({domainName}: {domainName: string}) => `続行する前に、DNS設定を更新して${domainName}の所有者であることを確認してください。`, + accessYourDNS: ({domainName}: {domainName: string}) => `DNSプロバイダーにアクセスし、${domainName} のDNS設定を開いてください。`, + addTXTRecord: '次のTXTレコードを追加してください:', + saveChanges: '変更を保存して、ここに戻り、ドメインを確認してください。', + youMayNeedToConsult: `検証を完了するには、組織のIT部門に相談する必要がある場合があります。詳細はこちら。`, + warning: '確認が完了すると、貴社のドメインのすべてのExpensifyメンバーに、アカウントが貴社のドメインで管理される旨のメールが送信されます。', + codeFetchError: '認証コードを取得できませんでした', + genericError: 'ドメインを確認できませんでした。もう一度お試しください。問題が解決しない場合は Concierge にご連絡ください。', + }, + domainVerified: { + title: 'ドメイン確認済み', + header: 'やった!あなたのドメインが確認されました', + description: ({domainName}: {domainName: string}) => + `ドメイン ${domainName} は正常に検証され、SAML やその他のセキュリティ機能を設定できるようになりました。`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 471322b3cc106..9feda8797df7c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7391,6 +7391,28 @@ ${amount} voor ${merchant} - ${date}`, subtitle: `We hebben niet al uw gegevens kunnen laden. We zijn op de hoogte gesteld en onderzoeken het probleem. Als dit aanhoudt, neem dan contact op met`, refreshAndTryAgain: 'Vernieuw en probeer het opnieuw', }, + domain: { + notVerified: 'Niet geverifieerd', + retry: 'Opnieuw proberen', + verifyDomain: { + title: 'Domein verifiëren', + beforeProceeding: ({domainName}: {domainName: string}) => + `Controleer voordat je verdergaat of je eigenaar bent van ${domainName} door de DNS-instellingen bij te werken.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Ga naar je DNS-provider en open de DNS-instellingen voor ${domainName}.`, + addTXTRecord: 'Voeg het volgende TXT-record toe:', + saveChanges: 'Sla wijzigingen op en kom hier terug om je domein te verifiëren.', + youMayNeedToConsult: `Misschien moet je contact opnemen met de IT-afdeling van je organisatie om de verificatie te voltooien. Meer informatie.`, + warning: 'Na verificatie ontvangen alle Expensify-leden op je domein een e-mail waarin staat dat hun account onder je domein wordt beheerd.', + codeFetchError: 'Kon de verificatiecode niet ophalen', + genericError: 'We konden uw domein niet verifiëren. Probeer het opnieuw en neem contact op met Concierge als het probleem aanhoudt.', + }, + domainVerified: { + title: 'Domein geverifieerd', + header: 'Woehoe! Je domein is geverifieerd', + description: ({domainName}: {domainName: string}) => + `Het domein ${domainName} is succesvol geverifieerd en je kunt nu SAML en andere beveiligingsfuncties instellen.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/pl.ts b/src/languages/pl.ts index b2754edfd0c8b..d2d95cecb20ad 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7379,6 +7379,28 @@ ${amount} dla ${merchant} - ${date}`, subtitle: `Nie udało nam się wczytać wszystkich Twoich danych. Zostaliśmy o tym powiadomieni i badamy problem. Jeśli problem będzie się utrzymywał, skontaktuj się z`, refreshAndTryAgain: 'Odśwież i spróbuj ponownie', }, + domain: { + notVerified: 'Niezweryfikowano', + retry: 'Spróbuj ponownie', + verifyDomain: { + title: 'Zweryfikuj domenę', + beforeProceeding: ({domainName}: {domainName: string}) => + `Zanim przejdziesz dalej, potwierdź, że jesteś właścicielem ${domainName}, aktualizując jego ustawienia DNS.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Uzyskaj dostęp do swojego dostawcy DNS i otwórz ustawienia DNS dla ${domainName}.`, + addTXTRecord: 'Dodaj następujący rekord TXT:', + saveChanges: 'Zapisz zmiany i wróć tutaj, aby zweryfikować swoją domenę.', + youMayNeedToConsult: `Może być konieczna konsultacja z działem IT Twojej organizacji, aby zakończyć weryfikację. Dowiedz się więcej.`, + warning: 'Po weryfikacji wszyscy członkowie Expensify w Twojej domenie otrzymają wiadomość e-mail z informacją, że ich konta będą zarządzane w ramach Twojej domeny.', + codeFetchError: 'Nie udało się pobrać kodu weryfikacyjnego', + genericError: 'Nie udało nam się zweryfikować Twojej domeny. Spróbuj ponownie i skontaktuj się z Concierge, jeśli problem będzie się utrzymywał.', + }, + domainVerified: { + title: 'Domena zweryfikowana', + header: 'Hurra! Twoja domena została zweryfikowana', + description: ({domainName}: {domainName: string}) => + `Domena ${domainName} została pomyślnie zweryfikowana i możesz teraz skonfigurować SAML oraz inne funkcje zabezpieczeń.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 1228d28a1e515..49a6b126db52b 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7394,6 +7394,28 @@ ${amount} para ${merchant} - ${date}`, subtitle: `Não conseguimos carregar todos os seus dados. Fomos notificados e estamos investigando o problema. Se isso persistir, entre em contato com`, refreshAndTryAgain: 'Atualize e tente novamente', }, + domain: { + notVerified: 'Não verificado', + retry: 'Tentar novamente', + verifyDomain: { + title: 'Verificar domínio', + beforeProceeding: ({domainName}: {domainName: string}) => + `Antes de prosseguir, verifique se você é o proprietário de ${domainName} atualizando as configurações de DNS.`, + accessYourDNS: ({domainName}: {domainName: string}) => `Acesse seu provedor de DNS e abra as configurações de DNS para ${domainName}.`, + addTXTRecord: 'Adicione o seguinte registro TXT:', + saveChanges: 'Salve as alterações e volte aqui para verificar seu domínio.', + youMayNeedToConsult: `Talvez seja necessário consultar o departamento de TI da sua organização para concluir a verificação. Saiba mais.`, + warning: 'Após a verificação, todos os membros do Expensify no seu domínio receberão um e-mail informando que suas contas serão gerenciadas sob seu domínio.', + codeFetchError: 'Não foi possível obter o código de verificação', + genericError: 'Não conseguimos verificar seu domínio. Tente novamente e entre em contato com o Concierge se o problema persistir.', + }, + domainVerified: { + title: 'Domínio verificado', + header: 'Uhul! Seu domínio foi verificado', + description: ({domainName}: {domainName: string}) => + `O domínio ${domainName} foi verificado com sucesso e agora você pode configurar SAML e outros recursos de segurança.`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 5797fb036805a..487a188e4cdd8 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7227,6 +7227,27 @@ ${merchant}的${amount} - ${date}`, }, avatarPage: {title: '编辑个人资料图片', upload: '上传', uploadPhoto: '上传照片', selectAvatar: '选择头像', chooseCustomAvatar: '或选择自定义头像'}, openAppFailureModal: {title: '出了点问题...', subtitle: `我们未能加载您的所有数据。我们已收到通知,正在调查此问题。如果问题仍然存在,请联系`, refreshAndTryAgain: '刷新并重试'}, + domain: { + notVerified: '未验证', + retry: '重试', + verifyDomain: { + title: '验证域名', + beforeProceeding: ({domainName}: {domainName: string}) => `在继续之前,请通过更新其 DNS 设置来验证您拥有 ${domainName}。`, + accessYourDNS: ({domainName}: {domainName: string}) => `访问您的 DNS 提供商,并打开 ${domainName} 的 DNS 设置。`, + addTXTRecord: '添加以下 TXT 记录:', + saveChanges: '保存更改并返回此处以验证您的域名。', + youMayNeedToConsult: `您可能需要咨询您组织的 IT 部门以完成验证。了解更多。`, + warning: '验证完成后,您的域中的所有 Expensify 成员将收到一封电子邮件,告知他们的账户将由您的域进行管理。', + codeFetchError: '无法获取验证码', + genericError: '我们无法验证您的域名。请重试,如果问题仍然存在,请联系 Concierge。', + }, + domainVerified: { + title: '域名已验证', + header: '哇哦!您的域名已通过验证', + description: ({domainName}: {domainName: string}) => + `域名 ${domainName} 已成功验证,您现在可以设置 SAML 和其他安全功能。`, + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/libs/API/parameters/DomainParams.ts b/src/libs/API/parameters/DomainParams.ts new file mode 100644 index 0000000000000..38fa1ad8780a4 --- /dev/null +++ b/src/libs/API/parameters/DomainParams.ts @@ -0,0 +1,5 @@ +type DomainParams = { + domainName: string; +}; + +export default DomainParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index e1f7fb3a1c4d1..9bc2d39d5b2b5 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -437,3 +437,4 @@ export type {default as AddReportApproverParams} from './AddReportApproverParams export type {default as EnableGlobalReimbursementsForUSDBankAccountParams} from './EnableGlobalReimbursementsForUSDBankAccountParams'; export type {default as SendReminderForCorpaySignerInformationParams} from './SendReminderForCorpaySignerInformationParams'; export type {default as SendScheduleCallNudgeParams} from './SendScheduleCallNudge'; +export type {default as DomainParams} from './DomainParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index e3ea06cb6cfa7..c3a659e47c255 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -516,6 +516,7 @@ const WRITE_COMMANDS = { ADD_REPORT_APPROVER: 'AddReportApprover', REQUEST_UNLOCK_ACCOUNT: 'RequestUnlockAccount', SEND_SCHEDULE_CALL_NUDGE: 'SendScheduleCallNudge', + VALIDATE_DOMAIN: 'ValidateDomain', } as const; type WriteCommand = ValueOf; @@ -1051,6 +1052,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ASSIGN_REPORT_TO_ME]: Parameters.AssignReportToMeParams; [WRITE_COMMANDS.ADD_REPORT_APPROVER]: Parameters.AddReportApproverParams; [WRITE_COMMANDS.REQUEST_UNLOCK_ACCOUNT]: Parameters.LockAccountParams; + + // Domain API + [WRITE_COMMANDS.VALIDATE_DOMAIN]: Parameters.DomainParams; }; const READ_COMMANDS = { @@ -1127,6 +1131,7 @@ const READ_COMMANDS = { OPEN_UNREPORTED_EXPENSES_PAGE: 'OpenUnreportedExpensesPage', GET_GUIDE_CALL_AVAILABILITY_SCHEDULE: 'GetGuideCallAvailabilitySchedule', GET_TRANSACTIONS_FOR_MERGING: 'GetTransactionsForMerging', + GET_DOMAIN_VALIDATE_CODE: 'GetDomainValidateCode', } as const; type ReadCommand = ValueOf; @@ -1205,6 +1210,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_UNREPORTED_EXPENSES_PAGE]: Parameters.OpenUnreportedExpensesPageParams; [READ_COMMANDS.GET_GUIDE_CALL_AVAILABILITY_SCHEDULE]: Parameters.GetGuideCallAvailabilityScheduleParams; [READ_COMMANDS.GET_TRANSACTIONS_FOR_MERGING]: Parameters.GetTransactionsForMergingParams; + [READ_COMMANDS.GET_DOMAIN_VALIDATE_CODE]: Parameters.DomainParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 42da8bf1ca97b..7434e4ff9c72b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -49,6 +49,7 @@ import type { WalletStatementNavigatorParamList, WorkspaceConfirmationNavigatorParamList, WorkspaceDuplicateNavigatorParamList, + WorkspacesDomainModalNavigatorParamList, } from '@navigation/types'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; @@ -922,6 +923,11 @@ const ScheduleCallModalStackNavigator = createModalStackNavigator require('../../../../pages/ScheduleCall/ScheduleCallConfirmationPage').default, }); +const WorkspacesDomainModalStackNavigator = createModalStackNavigator({ + [SCREENS.WORKSPACES_VERIFY_DOMAIN]: () => require('../../../../pages/domain/VerifyDomainPage').default, + [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: () => require('../../../../pages/domain/DomainVerifiedPage').default, +}); + export { AddPersonalBankAccountModalStackNavigator, EditRequestStackNavigator, @@ -968,4 +974,5 @@ export { AddUnreportedExpenseModalStackNavigator, ScheduleCallModalStackNavigator, MergeTransactionStackNavigator, + WorkspacesDomainModalStackNavigator, }; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 224d4e00c7b93..1e2b5003d38a4 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -256,6 +256,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.SCHEDULE_CALL} component={ModalStackNavigators.ScheduleCallModalStackNavigator} /> + {/* The second overlay is here to cover the wide rhp screen underneath */} diff --git a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts index 683ab77d84dc2..8f4b76aeceab6 100644 --- a/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/helpers/getAdaptedStateFromPath.ts @@ -16,6 +16,7 @@ import type {Report} from '@src/types/onyx'; import getMatchingNewRoute from './getMatchingNewRoute'; import getParamsFromRoute from './getParamsFromRoute'; import {isFullScreenName} from './isNavigatorName'; +import normalizePath from './normalizePath'; import replacePathInNestedState from './replacePathInNestedState'; let allReports: OnyxCollection; @@ -90,7 +91,9 @@ function getMatchingFullScreenRoute(route: NavigationPartialRoute) { if (RHP_TO_WORKSPACES_LIST[route.name]) { return { name: SCREENS.WORKSPACES_LIST, - path: ROUTES.WORKSPACES_LIST.route, + // prepending a slash to ensure closing the RHP after refreshing the page + // replaces the whole path with "/workspaces", instead of just replacing the last url segment ("/x/y/workspaces") + path: normalizePath(ROUTES.WORKSPACES_LIST.route), }; } 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 b920487f1d148..40eebe61d4be7 100644 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACES_LIST_TO_RHP.ts @@ -1,8 +1,7 @@ -import type {WorkspaceDuplicateNavigatorParamList} from '@navigation/types'; import SCREENS from '@src/SCREENS'; -const WORKSPACES_LIST_TO_RHP: Partial> = { - [SCREENS.WORKSPACE_DUPLICATE.ROOT]: [SCREENS.WORKSPACE_DUPLICATE.SELECT_FEATURES, SCREENS.WORKSPACE_DUPLICATE.ROOT], +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], }; export default WORKSPACES_LIST_TO_RHP; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 7030fe4573f4d..8f024598db677 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1729,6 +1729,18 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, }, }, + [SCREENS.RIGHT_MODAL.DOMAIN]: { + screens: { + [SCREENS.WORKSPACES_VERIFY_DOMAIN]: { + path: ROUTES.WORKSPACES_VERIFY_DOMAIN.route, + exact: true, + }, + [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: { + path: ROUTES.WORKSPACES_DOMAIN_VERIFIED.route, + exact: true, + }, + }, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9d42438645d59..e29911da78079 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2053,6 +2053,15 @@ type MergeTransactionNavigatorParamList = { }; }; +type WorkspacesDomainModalNavigatorParamList = { + [SCREENS.WORKSPACES_VERIFY_DOMAIN]: { + accountID: number; + }; + [SCREENS.WORKSPACES_DOMAIN_VERIFIED]: { + accountID: number; + }; +}; + type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH]: NavigatorScreenParams; @@ -2101,6 +2110,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.DOMAIN]: NavigatorScreenParams; }; type TravelNavigatorParamList = { @@ -2803,4 +2813,5 @@ export type { TestToolsModalModalNavigatorParamList, MergeTransactionNavigatorParamList, AttachmentModalScreensParamList, + WorkspacesDomainModalNavigatorParamList, }; diff --git a/src/libs/actions/Domain.ts b/src/libs/actions/Domain.ts new file mode 100644 index 0000000000000..0a1c0ec026cf6 --- /dev/null +++ b/src/libs/actions/Domain.ts @@ -0,0 +1,79 @@ +import Onyx from 'react-native-onyx'; +import type {OnyxUpdate} from 'react-native-onyx'; +import * as API from '@libs/API'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; + +/** + * Fetches a validation code that the user is supposed to put in the domain's DNS records to verify it + */ +function getDomainValidationCode(accountID: number, domainName: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, + value: {isValidateCodeLoading: true, validateCodeError: null}, + }, + ]; + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, + value: {isValidateCodeLoading: null}, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, + value: { + isValidateCodeLoading: null, + validateCodeError: getMicroSecondOnyxErrorWithTranslationKey('domain.verifyDomain.codeFetchError'), + }, + }, + ]; + + API.read(READ_COMMANDS.GET_DOMAIN_VALIDATE_CODE, {domainName}, {optimisticData, successData, failureData}); +} + +/** + * Checks if the validation code is present in the domain's DNS records to mark the domain as validated and the user as a verified admin + */ +function validateDomain(accountID: number, domainName: string) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, + value: {isValidationPending: true, domainValidationError: null}, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, + value: {isValidationPending: null}, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, + value: { + isValidationPending: null, + domainValidationError: getMicroSecondOnyxErrorWithTranslationKey('domain.verifyDomain.genericError'), + }, + }, + ]; + + API.write(WRITE_COMMANDS.VALIDATE_DOMAIN, {domainName}, {optimisticData, successData, failureData}); +} + +function resetDomainValidationError(accountID: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.DOMAIN}${accountID}`, {domainValidationError: null}); +} + +export {getDomainValidationCode, validateDomain, resetDomainValidationError}; diff --git a/src/pages/domain/DomainVerifiedPage.tsx b/src/pages/domain/DomainVerifiedPage.tsx new file mode 100644 index 0000000000000..9c865ff8d5695 --- /dev/null +++ b/src/pages/domain/DomainVerifiedPage.tsx @@ -0,0 +1,71 @@ +import {Str} from 'expensify-common'; +import React, {useEffect} from 'react'; +import {View} from 'react-native'; +import ConfirmationPage from '@components/ConfirmationPage'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import LottieAnimations from '@components/LottieAnimations'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +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'; + +type DomainVerifiedPageProps = PlatformStackScreenProps; + +function DomainVerifiedPage({route}: DomainVerifiedPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const accountID = route.params.accountID; + const [domain, domainMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.DOMAIN}${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]); + + if (domainMetadata.status === 'loading') { + return ; + } + + if (!domain) { + return Navigation.dismissModal()} />; + } + + return ( + + + + + + } + innerContainerStyle={styles.p10} + buttonText={translate('common.buttonConfirm')} + shouldShowButton + onButtonPress={() => Navigation.dismissModal()} + /> + + ); +} + +DomainVerifiedPage.displayName = 'DomainVerifiedPage'; +export default DomainVerifiedPage; diff --git a/src/pages/domain/VerifyDomainPage.tsx b/src/pages/domain/VerifyDomainPage.tsx new file mode 100644 index 0000000000000..60e01667947dd --- /dev/null +++ b/src/pages/domain/VerifyDomainPage.tsx @@ -0,0 +1,172 @@ +import {Str} from 'expensify-common'; +import React, {useEffect} from 'react'; +import type {PropsWithChildren} from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import CopyableTextField from '@components/Domain/CopyableTextField'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import FormHelpMessage from '@components/FormHelpMessage'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import {Exclamation} from '@components/Icon/Expensicons'; +import RenderHTML from '@components/RenderHTML'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import useTheme from '@hooks/useTheme'; +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'; + +function OrderedListRow({index, children}: PropsWithChildren<{index: number}>) { + const styles = useThemeStyles(); + return ( + + {index}. + {children} + + ); +} + +type VerifyDomainPageProps = PlatformStackScreenProps; + +function VerifyDomainPage({route}: VerifyDomainPageProps) { + 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 doesDomainExist = !!domain; + + useEffect(() => { + if (!domain?.validated) { + return; + } + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(ROUTES.WORKSPACES_DOMAIN_VERIFIED.getRoute(accountID), {forceReplace: true})); + }, [accountID, domain?.validated]); + + useEffect(() => { + if (!doesDomainExist) { + return; + } + getDomainValidationCode(accountID, domainName); + }, [accountID, domainName, doesDomainExist]); + + useEffect(() => { + if (!doesDomainExist) { + return; + } + resetDomainValidationError(accountID); + }, [accountID, doesDomainExist]); + + if (domainMetadata.status === 'loading') { + return ; + } + + if (!domain) { + return Navigation.dismissModal()} />; + } + + return ( + + + + + + + + + + + + + + + + + + + {translate('domain.verifyDomain.addTXTRecord')} + + {!domain.validateCodeError && ( + + )} + + + + {!!domain.validateCodeError && ( + + +