import { find } from "lodash"; import { observer } from "mobx-react"; import { BackIcon, EmailIcon } from "outline-icons"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { useLocation, Link, Redirect } from "react-router-dom"; import styled from "styled-components"; import { getCookie, setCookie } from "tiny-cookie"; import { s } from "@shared/styles"; import { parseDomain } from "@shared/utils/domains"; import { Config } from "~/stores/AuthStore"; import ButtonLarge from "~/components/ButtonLarge"; import Fade from "~/components/Fade"; import Flex from "~/components/Flex"; import Heading from "~/components/Heading"; import OutlineIcon from "~/components/Icons/OutlineIcon"; import LoadingIndicator from "~/components/LoadingIndicator"; import PageTitle from "~/components/PageTitle"; import TeamLogo from "~/components/TeamLogo"; import Text from "~/components/Text"; import env from "~/env"; import useLastVisitedPath from "~/hooks/useLastVisitedPath"; import useQuery from "~/hooks/useQuery"; import useStores from "~/hooks/useStores"; import { draggableOnDesktop } from "~/styles"; import Desktop from "~/utils/Desktop"; import isCloudHosted from "~/utils/isCloudHosted"; import { changeLanguage, detectLanguage } from "~/utils/language"; import AuthenticationProvider from "./AuthenticationProvider"; import Notices from "./Notices"; function Header({ config }: { config?: Config | undefined }) { const { t } = useTranslation(); const isSubdomain = !!config?.hostname; if ( !isCloudHosted || parseDomain(window.location.origin).custom || Desktop.isElectron() ) { return null; } return ( {t("Back to home")} ); } type Props = { children?: (config?: Config) => React.ReactNode; }; function Login({ children }: Props) { const location = useLocation(); const query = useQuery(); const { t, i18n } = useTranslation(); const { auth } = useStores(); const { config } = auth; const [error, setError] = React.useState(null); const [emailLinkSentTo, setEmailLinkSentTo] = React.useState(""); const isCreate = location.pathname === "/create"; const rememberLastPath = !!auth.user?.preferences?.rememberLastPath; const [lastVisitedPath] = useLastVisitedPath(); const handleReset = React.useCallback(() => { setEmailLinkSentTo(""); }, []); const handleEmailSuccess = React.useCallback((email) => { setEmailLinkSentTo(email); }, []); React.useEffect(() => { auth.fetchConfig().catch(setError); }, [auth]); // TODO: Persist detected language to new user // Try to detect the user's language and show the login page on its idiom // if translation is available React.useEffect(() => { changeLanguage(detectLanguage(), i18n); }, [i18n]); React.useEffect(() => { const entries = Object.fromEntries(query.entries()); const existing = getCookie("signupQueryParams"); // We don't want to set this cookie if we're viewing an error notice via // query string(notice =), if there are no query params, or it's already set if (Object.keys(entries).length && !query.get("notice") && !existing) { setCookie("signupQueryParams", JSON.stringify(entries)); } }, [query]); if ( auth.authenticated && rememberLastPath && lastVisitedPath !== location.pathname ) { return ; } if (auth.authenticated && auth.team?.defaultCollectionId) { return ; } if (auth.authenticated) { return ; } if (error) { return ( {t("Error")} {t("Failed to load configuration.")} {!isCloudHosted && ( {t( "Check the network requests and server logs for full details of the error." )} )} ); } // we're counting on the config request being fast, so just a simple loading // indicator here that's delayed by 250ms if (!config) { return ; } const isCustomDomain = parseDomain(window.location.origin).custom; // Unmapped custom domain if (isCloudHosted && isCustomDomain && !config.name) { return ( {t("Almost there")}… {t( "Your custom domain is successfully pointing at Outline. To complete the setup process please contact support." )} ); } const hasMultipleProviders = config.providers.length > 1; const defaultProvider = find( config.providers, (provider) => provider.id === auth.lastSignedIn && !isCreate ); if (emailLinkSentTo) { return ( {t("Check your email")} }} /> {t("Back to login")} ); } return ( {config.logo && !isCreate ? ( ) : ( )} {isCreate ? ( <> {t("Create a workspace")} {t( "Get started by choosing a sign-in method for your new workspace below…" )} > ) : ( <> {t("Login to {{ authProviderName }}", { authProviderName: config.name || env.APP_NAME, })} {children?.(config)} > )} {defaultProvider && ( {hasMultipleProviders && ( <> {t("You signed in with {{ authProviderName }} last time.", { authProviderName: defaultProvider.name, })} > )} )} {config.providers.map((provider) => { if (defaultProvider && provider.id === defaultProvider.id) { return null; } return ( ); })} {isCreate && ( Already have an account? Go to login. )} ); } const StyledHeading = styled(Heading)` margin: 0; `; const CheckEmailIcon = styled(EmailIcon)` margin-bottom: -1.5em; `; const Background = styled(Fade)` width: 100vw; height: 100%; background: ${s("background")}; display: flex; ${draggableOnDesktop()} `; const Logo = styled.div` margin-bottom: -4px; `; const Content = styled(Text)` color: ${s("textSecondary")}; text-align: center; margin-top: -8px; `; const Note = styled(Text)` color: ${s("textTertiary")}; text-align: center; font-size: 14px; margin-top: 8px; em { font-style: normal; font-weight: 500; } `; const Back = styled.a` display: flex; align-items: center; color: inherit; padding: 32px; font-weight: 500; position: absolute; svg { transition: transform 100ms ease-in-out; } &:hover { svg { transform: translateX(-4px); } } `; const Or = styled.hr` margin: 1em 0; position: relative; width: 100%; &:after { content: attr(data-text); display: block; position: absolute; left: 50%; transform: translate3d(-50%, -50%, 0); text-transform: uppercase; font-size: 11px; color: ${s("textSecondary")}; background: ${s("background")}; border-radius: 2px; padding: 0 4px; } `; const Centered = styled(Flex)` user-select: none; width: 90vw; height: 100%; max-width: 320px; margin: 0 auto; `; export default observer(Login);
{t( "Check the network requests and server logs for full details of the error." )}