diff --git a/package.json b/package.json
index 62948954bba50d1571bd8e5a961185e38085d173..26f8587075cc2a619a976e6e66d3a340d64e61d5 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
   },
   "devDependencies": {
     "@fontsource/open-sans": "^4.4.5",
+    "@hcaptcha/react-hcaptcha": "^0.3.6",
     "@preact/preset-vite": "^2.0.0",
     "@styled-icons/bootstrap": "^10.34.0",
     "@styled-icons/feather": "^10.34.0",
@@ -36,6 +37,7 @@
     "@typescript-eslint/eslint-plugin": "^4.27.0",
     "@typescript-eslint/parser": "^4.27.0",
     "dayjs": "^1.10.5",
+    "detect-browser": "^5.2.0",
     "eslint": "^7.28.0",
     "eslint-config-preact": "^1.1.4",
     "localforage": "^1.9.0",
@@ -43,6 +45,7 @@
     "prettier": "^2.3.1",
     "react-device-detect": "^1.17.0",
     "react-helmet": "^6.1.0",
+    "react-hook-form": "6.3.0",
     "react-overlapping-panels": "1.1.2-patch.0",
     "react-redux": "^7.2.4",
     "react-router-dom": "^5.2.0",
diff --git a/src/app.tsx b/src/app.tsx
index f0f1f387aafcd20a369b995c5f19ae921a5e2965..605c43e0ba54823cc15c5b6da2f8e559be90a0c9 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -2,13 +2,15 @@ import { CheckAuth } from "./context/revoltjs/CheckAuth";
 import { Route, Switch } from "react-router-dom";
 import Context from "./context";
 
+import { Login } from "./pages/login/Login";
+
 export function App() {
     return (
         <Context>
             <Switch>
                 <Route path="/login">
                     <CheckAuth>
-                        <h1>login</h1>
+                        <Login />
                     </CheckAuth>
                 </Route>
                 <Route path="/">
diff --git a/src/context/Theme.tsx b/src/context/Theme.tsx
index 76fb20e1cd68758869b7c037b22d5d07d75f51e0..42365cb9746b745bdd28fdeef9ca66d5dd3f448a 100644
--- a/src/context/Theme.tsx
+++ b/src/context/Theme.tsx
@@ -1,6 +1,7 @@
 import { isTouchscreenDevice } from "../lib/isTouchscreenDevice";
 import { createGlobalStyle } from "styled-components";
 import { Children } from "../types/Preact";
+import { createContext } from "preact";
 import { Helmet } from "react-helmet";
 
 export type Variables =
@@ -111,6 +112,8 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
 }
 `;
 
+export const ThemeContext = createContext<Theme>({} as any);
+
 interface Props {
     children: Children;
 }
@@ -119,7 +122,7 @@ export default function Theme(props: Props) {
     const theme = PRESETS.dark;
 
     return (
-        <>
+        <ThemeContext.Provider value={theme}>
             <Helmet>
                 <meta
                     name="theme-color"
@@ -132,6 +135,6 @@ export default function Theme(props: Props) {
             </Helmet>
             <GlobalTheme theme={theme} />
             {props.children}
-        </>
+        </ThemeContext.Provider>
     );
 }
diff --git a/src/context/revoltjs/error.ts b/src/context/revoltjs/error.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0c44c77cc10e5c04eed657ae1cae61e4ab05722a
--- /dev/null
+++ b/src/context/revoltjs/error.ts
@@ -0,0 +1,18 @@
+export function takeError(
+    error: any
+): string {
+    const type = error?.response?.data?.type;
+    let id = type;
+    if (!type) {
+        if (error?.response?.status === 403) {
+            return "Unauthorized";
+        } else if (error && (!!error.isAxiosError && !error.response)) {
+            return "NetworkError";
+        }
+
+        console.error(error);
+        return "UnknownError";
+    }
+
+    return id;
+}
diff --git a/src/pages/login/FormField.tsx b/src/pages/login/FormField.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..eb7f498899a4e8598ec0c842fe26fa7ebfa2d7a7
--- /dev/null
+++ b/src/pages/login/FormField.tsx
@@ -0,0 +1,68 @@
+import Overline from '../../components/ui/Overline';
+import InputBox from '../../components/ui/InputBox';
+import { Text, Localizer } from 'preact-i18n';
+
+interface Props {
+    type: "email" | "username" | "password" | "invite" | "current_password";
+    showOverline?: boolean;
+    register: Function;
+    error?: string;
+    name?: string;
+}
+
+export default function FormField({
+    type,
+    register,
+    showOverline,
+    error,
+    name
+}: Props) {
+    return (
+        <>
+            {showOverline && (
+                <Overline error={error}>
+                    <Text id={`login.${type}`} />
+                </Overline>
+            )}
+            <Localizer>
+                <InputBox
+                    placeholder={(<Text id={`login.enter.${type}`} />) as any}
+                    name={
+                        type === "current_password" ? "password" : name ?? type
+                    }
+                    type={
+                        type === "invite" || type === "username"
+                            ? "text"
+                            : type === "current_password"
+                            ? "password"
+                            : type
+                    }
+                    ref={register(
+                        type === "password" || type === "current_password"
+                            ? {
+                                  validate: (value: string) =>
+                                      value.length === 0
+                                          ? "RequiredField"
+                                          : value.length < 8
+                                          ? "TooShort"
+                                          : value.length > 1024
+                                          ? "TooLong"
+                                          : undefined
+                              }
+                            : type === "email"
+                            ? {
+                                  required: "RequiredField",
+                                  pattern: {
+                                      value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
+                                      message: "InvalidEmail"
+                                  }
+                              }
+                            : type === "username"
+                            ? { required: "RequiredField" }
+                            : { required: "RequiredField" }
+                    )}
+                />
+            </Localizer>
+        </>
+    );
+}
diff --git a/src/pages/login/Login.module.scss b/src/pages/login/Login.module.scss
new file mode 100644
index 0000000000000000000000000000000000000000..3b982bbb3159c37ae6d97ac635a85eb5f73b1613
--- /dev/null
+++ b/src/pages/login/Login.module.scss
@@ -0,0 +1,123 @@
+.login {
+    display: flex;
+    flex-direction: row;
+
+    svg {
+        margin: auto;
+    }
+
+    > div {
+        flex: 1;
+    }
+
+    .content {
+        display: flex;
+        flex-direction: column;
+
+        justify-content: space-between;
+
+        .attribution {
+            color: var(--tertiary-background);
+            font-size: 12px;
+            line-height: 12px;
+            margin: 8px;
+
+            display: flex;
+            flex-direction: row;
+            justify-content: space-between;
+        }
+
+        .modal {
+            display: flex;
+            flex-direction: row;
+
+            justify-content: center;
+        }
+    }
+
+    .bg {
+        background-size: cover !important;
+    }
+}
+
+.form {
+    display: flex;
+    flex-direction: column;
+    font-size: 14px;
+
+    img {
+        width: 260px;
+        margin: auto;
+    }
+
+    a {
+        margin-top: 4px;
+    }
+
+    form {
+        margin: 1em 0;
+        display: flex;
+        flex-direction: column;
+
+        button {
+            margin-top: 24px;
+        }
+    }
+
+    .create {
+        text-align: center;
+        color: var(--tertiary-foreground);
+
+        a {
+            margin: 0 4px;
+        }
+    }
+}
+
+.success {
+    display: flex;
+    align-items: center;
+    flex-direction: column;
+
+    .note {
+        color: var(--tertiary-foreground);
+    }
+
+    .mailProvider {
+        padding: 24px 0;
+    }
+
+    * {
+        margin: 0;
+    }
+
+    h1 {
+        font-weight: 400;
+    }
+
+    h2 {
+        font-weight: 300;
+    }
+}
+
+.footer {
+    margin-top: 12px;
+    text-align: center;
+    color: var(--tertiary6);
+
+    a {
+        color: var(--tertiary-background) !important;
+        cursor: pointer;
+        margin: 0 2px;
+
+        &:hover {
+            color: var(--tertiary-foreground) !important;
+        }
+    }
+}
+
+@media only screen and (max-width: 768px) {
+    .bg {
+        display: none;
+    }
+}
diff --git a/src/pages/login/Login.tsx b/src/pages/login/Login.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6e463dd0fcbefce4fa42ea7fb0643e4ea674435f
--- /dev/null
+++ b/src/pages/login/Login.tsx
@@ -0,0 +1,70 @@
+import { Text } from "preact-i18n";
+import { Helmet } from "react-helmet";
+import styles from "./Login.module.scss";
+import { useContext } from "preact/hooks";
+import { APP_VERSION } from "../../version";
+import { LIBRARY_VERSION } from "revolt.js";
+import { Route, Switch } from "react-router-dom";
+import { ThemeContext } from "../../context/Theme";
+import { RevoltClient } from "../../context/revoltjs/RevoltClient";
+
+import background from "./background.jpg";
+
+import { FormLogin } from "./forms/FormLogin";
+import { FormCreate } from "./forms/FormCreate";
+import { FormResend } from "./forms/FormResend";
+import { FormReset, FormSendReset } from "./forms/FormReset";
+
+export const Login = () => {
+    const theme = useContext(ThemeContext);
+
+    return (
+        <div className={styles.login}>
+            <Helmet>
+                <meta name="theme-color" content={theme.background} />
+            </Helmet>
+            <div className={styles.content}>
+                <div className={styles.attribution}>
+                    <span>
+                        API:{" "}
+                        <code>{RevoltClient.configuration?.revolt ?? "???"}</code>{" "}
+                        &middot; revolt.js: <code>{LIBRARY_VERSION}</code>{" "}
+                        &middot; App: <code>{APP_VERSION}</code>
+                    </span>
+                    <span>
+                        {/*<LocaleSelector />*/}
+                    </span>
+                </div>
+                <div className={styles.modal}>
+                    <Switch>
+                        <Route path="/login/create">
+                            <FormCreate />
+                        </Route>
+                        <Route path="/login/resend">
+                            <FormResend />
+                        </Route>
+                        <Route path="/login/reset/:token">
+                            <FormReset />
+                        </Route>
+                        <Route path="/login/reset">
+                            <FormSendReset />
+                        </Route>
+                        <Route path="/">
+                            <FormLogin />
+                        </Route>
+                    </Switch>
+                </div>
+                <div className={styles.attribution}>
+                    <span>
+                        <Text id="general.image_by" /> &lrm;@lorenzoherrera
+                        &rlm;· unsplash.com
+                    </span>
+                </div>
+            </div>
+            <div
+                className={styles.bg}
+                style={{ background: `url('${background}')` }}
+            />
+        </div>
+    );
+};
diff --git a/src/pages/login/background.jpg b/src/pages/login/background.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..44254ed74f6a3db4b822b8d8123e363933cd5ef8
Binary files /dev/null and b/src/pages/login/background.jpg differ
diff --git a/src/pages/login/forms/CaptchaBlock.tsx b/src/pages/login/forms/CaptchaBlock.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7030dbd5b18d1f2aff9634704aaed56e41a2d44a
--- /dev/null
+++ b/src/pages/login/forms/CaptchaBlock.tsx
@@ -0,0 +1,36 @@
+import { Text } from "preact-i18n";
+import { useEffect } from "preact/hooks";
+import styles from "../Login.module.scss";
+import HCaptcha from "@hcaptcha/react-hcaptcha";
+import Preloader from "../../../components/ui/Preloader";
+import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
+
+export interface CaptchaProps {
+    onSuccess: (token?: string) => void;
+    onCancel: () => void;
+}
+
+export function CaptchaBlock(props: CaptchaProps) {
+    useEffect(() => {
+        if (!RevoltClient.configuration?.features.captcha.enabled) {
+            props.onSuccess();
+        }
+    }, []);
+
+    if (!RevoltClient.configuration?.features.captcha.enabled)
+        return <Preloader />;
+
+    return (
+        <div>
+            <HCaptcha
+                sitekey={RevoltClient.configuration.features.captcha.key}
+                onVerify={token => props.onSuccess(token)}
+            />
+            <div className={styles.footer}>
+                <a onClick={props.onCancel}>
+                    <Text id="login.cancel" />
+                </a>
+            </div>
+        </div>
+    );
+}
diff --git a/src/pages/login/forms/Form.tsx b/src/pages/login/forms/Form.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff28139c544397d031b84db7de6f0fcb8aa844c7
--- /dev/null
+++ b/src/pages/login/forms/Form.tsx
@@ -0,0 +1,236 @@
+import { Legal } from "./Legal";
+import { Text } from "preact-i18n";
+import { Link } from "react-router-dom";
+import { useState } from "preact/hooks";
+import styles from "../Login.module.scss";
+import { useForm } from "react-hook-form";
+import { MailProvider } from "./MailProvider";
+import { CheckCircle, Mail } from "@styled-icons/feather";
+import { CaptchaBlock, CaptchaProps } from "./CaptchaBlock";
+import { takeError } from "../../../context/revoltjs/error";
+import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
+
+import FormField from "../FormField";
+import Button from "../../../components/ui/Button";
+import Overline from "../../../components/ui/Overline";
+import Preloader from "../../../components/ui/Preloader";
+
+interface Props {
+    page: "create" | "login" | "send_reset" | "reset" | "resend";
+    callback: (fields: {
+        email: string;
+        password: string;
+        invite: string;
+        captcha?: string;
+    }) => Promise<void>;
+}
+
+function getInviteCode() {
+    if (typeof window === 'undefined') return '';
+
+    const urlParams = new URLSearchParams(window.location.search);
+    const code = urlParams.get('code');
+    return code ?? '';
+}
+
+export function Form({ page, callback }: Props) {
+    const [loading, setLoading] = useState(false);
+    const [success, setSuccess] = useState<string | undefined>(undefined);
+    const [error, setGlobalError] = useState<string | undefined>(undefined);
+    const [captcha, setCaptcha] = useState<CaptchaProps | undefined>(undefined);
+
+    const { handleSubmit, register, errors, setError } = useForm({
+        defaultValues: {
+            email: '',
+            password: '',
+            invite: getInviteCode()
+        }
+    });
+
+    async function onSubmit(data: {
+        email: string;
+        password: string;
+        invite: string;
+    }) {
+        setGlobalError(undefined);
+        setLoading(true);
+
+        function onError(err: any) {
+            setLoading(false);
+
+            const error = takeError(err);
+            switch (error) {
+                case "email_in_use":
+                    return setError("email", { type: "", message: error });
+                case "unknown_user":
+                    return setError("email", { type: "", message: error });
+                case "invalid_invite":
+                    return setError("invite", { type: "", message: error });
+            }
+
+            setGlobalError(error);
+        }
+
+        try {
+            if (
+                RevoltClient.configuration?.features.captcha.enabled &&
+                page !== "reset"
+            ) {
+                setCaptcha({
+                    onSuccess: async captcha => {
+                        setCaptcha(undefined);
+                        try {
+                            await callback({ ...data, captcha });
+                            setSuccess(data.email);
+                        } catch (err) {
+                            onError(err);
+                        }
+                    },
+                    onCancel: () => {
+                        setCaptcha(undefined);
+                        setLoading(false);
+                    }
+                });
+            } else {
+                await callback(data);
+                setSuccess(data.email);
+            }
+        } catch (err) {
+            onError(err);
+        }
+    }
+
+    if (typeof success !== "undefined") {
+        return (
+            <div className={styles.success}>
+                {RevoltClient.configuration?.features.email ? (
+                    <>
+                        <Mail size={72} />
+                        <h2>
+                            <Text id="login.check_mail" />
+                        </h2>
+                        <p className={styles.note}>
+                            <Text id="login.email_delay" />
+                        </p>
+                        <MailProvider email={success} />
+                    </>
+                ) : (
+                    <>
+                        <CheckCircle size={72} />
+                        <h1>
+                            <Text id="login.successful_registration" />
+                        </h1>
+                    </>
+                )}
+                <span className={styles.footer}>
+                    <Link to="/login">
+                        <a>
+                            <Text id="login.remembered" />
+                        </a>
+                    </Link>
+                </span>
+            </div>
+        );
+    }
+
+    if (captcha) return <CaptchaBlock {...captcha} />;
+    if (loading) return <Preloader />;
+
+    return (
+        <div className={styles.form}>
+            <form onSubmit={handleSubmit(onSubmit) as any}>
+                {page !== "reset" && (
+                    <FormField
+                        type="email"
+                        register={register}
+                        showOverline
+                        error={errors.email?.message}
+                    />
+                )}
+                {(page === "login" ||
+                    page === "create" ||
+                    page === "reset") && (
+                    <FormField
+                        type="password"
+                        register={register}
+                        showOverline
+                        error={errors.password?.message}
+                    />
+                )}
+                {RevoltClient.configuration?.features.invite_only &&
+                    page === "create" && (
+                        <FormField
+                            type="invite"
+                            register={register}
+                            showOverline
+                            error={errors.invite?.message}
+                        />
+                    )}
+                {error && (
+                    <Overline type="error" error={error}>
+                        <Text id={`login.error.${page}`} />
+                    </Overline>
+                )}
+                <Button>
+                    <Text
+                        id={
+                            page === "create"
+                                ? "login.register"
+                                : page === "login"
+                                ? "login.title"
+                                : page === "reset"
+                                ? "login.set_password"
+                                : page === "resend"
+                                ? "login.resend"
+                                : "login.reset"
+                        }
+                    />
+                </Button>
+            </form>
+            {page === "create" && (
+                <>
+                    <span className={styles.create}>
+                        <Text id="login.existing" />
+                        <Link to="/login">
+                            <Text id="login.title" />
+                        </Link>
+                    </span>
+                    <span className={styles.create}>
+                        <Text id="login.missing_verification" />
+                        <Link to="/login/resend">
+                            <Text id="login.resend" />
+                        </Link>
+                    </span>
+                </>
+            )}
+            {page === "login" && (
+                <>
+                    <span className={styles.create}>
+                        <Text id="login.new" />
+                        <Link to="/login/create">
+                            <Text id="login.create" />
+                        </Link>
+                    </span>
+                    <span className={styles.create}>
+                        <Text id="login.forgot" />
+                        <Link to="/login/reset">
+                            <Text id="login.reset" />
+                        </Link>
+                    </span>
+                </>
+            )}
+            {(page === "reset" ||
+                page === "resend" ||
+                page === "send_reset") && (
+                <>
+                    <span className={styles.create}>
+                        <Link to="/login">
+                            <Text id="login.remembered" />
+                        </Link>
+                    </span>
+                </>
+            )}
+            <Legal />
+        </div>
+    );
+}
diff --git a/src/pages/login/forms/FormCreate.tsx b/src/pages/login/forms/FormCreate.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0d586c5c390150915fdd64e395aa16981e10ab8d
--- /dev/null
+++ b/src/pages/login/forms/FormCreate.tsx
@@ -0,0 +1,13 @@
+import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
+import { Form } from "./Form";
+
+export function FormCreate() {
+    return (
+        <Form
+            page="create"
+            callback={async data => {
+                await RevoltClient.register(process.env.API_SERVER as string, data);
+            }}
+        />
+    );
+}
diff --git a/src/pages/login/forms/FormLogin.tsx b/src/pages/login/forms/FormLogin.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..04e6f386e7219ecb40f3d4e63dc9cdccf7fe1f88
--- /dev/null
+++ b/src/pages/login/forms/FormLogin.tsx
@@ -0,0 +1,29 @@
+import { Form } from "./Form";
+import { useContext } from "preact/hooks";
+import { useHistory } from "react-router-dom";
+import { deviceDetect } from "react-device-detect";
+import { AppContext } from "../../../context/revoltjs/RevoltClient";
+
+export function FormLogin() {
+    const { operations } = useContext(AppContext);
+    const history = useHistory();
+
+    return (
+        <Form
+            page="login"
+            callback={async data => {
+                const browser = deviceDetect();
+                let device_name;
+                if (browser) {
+                    const { name, os } = browser;
+                    device_name = `${name} on ${os}`;
+                } else {
+                    device_name = "Unknown Device";
+                }
+
+                await operations.login({ ...data, device_name });
+                history.push("/");
+            }}
+        />
+    );
+}
diff --git a/src/pages/login/forms/FormResend.tsx b/src/pages/login/forms/FormResend.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..badbabf30bfa37a2a45bc511b1acab79996bedc7
--- /dev/null
+++ b/src/pages/login/forms/FormResend.tsx
@@ -0,0 +1,13 @@
+import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
+import { Form } from "./Form";
+
+export function FormResend() {
+    return (
+        <Form
+            page="resend"
+            callback={async data => {
+                await RevoltClient.req("POST", "/auth/resend", data);
+            }}
+        />
+    );
+}
diff --git a/src/pages/login/forms/FormReset.tsx b/src/pages/login/forms/FormReset.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..01ddee7676e452518e8f9d24aa99228498a01cf4
--- /dev/null
+++ b/src/pages/login/forms/FormReset.tsx
@@ -0,0 +1,32 @@
+import { Form } from "./Form";
+import { useHistory, useParams } from "react-router-dom";
+import { RevoltClient } from "../../../context/revoltjs/RevoltClient";
+
+export function FormSendReset() {
+    return (
+        <Form
+            page="send_reset"
+            callback={async data => {
+                await RevoltClient.req("POST", "/auth/send_reset", data);
+            }}
+        />
+    );
+}
+
+export function FormReset() {
+    const { token } = useParams<{ token: string }>();
+    const history = useHistory();
+
+    return (
+        <Form
+            page="reset"
+            callback={async data => {
+                await RevoltClient.req("POST", "/auth/reset" as any, {
+                    token,
+                    ...(data as any)
+                });
+                history.push("/login");
+            }}
+        />
+    );
+}
diff --git a/src/pages/login/forms/Legal.tsx b/src/pages/login/forms/Legal.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2517352c1d8bec309364e78af102d63f6cb362f5
--- /dev/null
+++ b/src/pages/login/forms/Legal.tsx
@@ -0,0 +1,29 @@
+import styles from "../Login.module.scss";
+import { Text } from "preact-i18n";
+
+export function Legal() {
+    return (
+        <span className={styles.footer}>
+            <a
+                href="https://revolt.chat/about"
+                target="_blank"
+            >
+                <Text id="general.about" />
+            </a>
+            &middot;
+            <a
+                href="https://revolt.chat/terms"
+                target="_blank"
+            >
+                <Text id="general.tos" />
+            </a>
+            &middot;
+            <a
+                href="https://revolt.chat/privacy"
+                target="_blank"
+            >
+                <Text id="general.privacy" />
+            </a>
+        </span>
+    );
+}
diff --git a/src/pages/login/forms/MailProvider.tsx b/src/pages/login/forms/MailProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..261857ba1dc545fc7ddfeea0e681af4c6f76d6f0
--- /dev/null
+++ b/src/pages/login/forms/MailProvider.tsx
@@ -0,0 +1,55 @@
+import { Text } from "preact-i18n";
+import styles from "../Login.module.scss";
+import Button from "../../../components/ui/Button";
+
+interface Props {
+    email?: string;
+}
+
+function mapMailProvider(email?: string): [string, string] | undefined {
+    if (!email) return;
+
+    const match = /@(.+)/.exec(email);
+    if (match === null) return;
+
+    const domain = match[1];
+    switch (domain) {
+        case "gmail.com":
+            return ["Gmail", "https://gmail.com"];
+        case "tuta.io":
+            return ["Tutanota", "https://mail.tutanota.com"];
+        case "outlook.com":
+            return ["Outlook", "https://outlook.live.com"];
+        case "yahoo.com":
+            return ["Yahoo", "https://mail.yahoo.com"];
+        case "wp.pl":
+            return ["WP Poczta", "https://poczta.wp.pl"];
+        case "protonmail.com":
+        case "protonmail.ch":
+            return ["ProtonMail", "https://mail.protonmail.com"];
+        case "seznam.cz":
+        case "email.cz":
+        case "post.cz":
+            return ["Seznam", "https://email.seznam.cz"];
+        default:
+            return [domain, `https://${domain}`];
+    }
+}
+
+export function MailProvider({ email }: Props) {
+    const provider = mapMailProvider(email);
+    if (!provider) return null;
+
+    return (
+        <div className={styles.mailProvider}>
+            <a href={provider[1]} target="_blank">
+                <Button>
+                    <Text
+                        id="login.open_mail_provider"
+                        fields={{ provider: provider[0] }}
+                    />
+                </Button>
+            </a>
+        </div>
+    );
+}
diff --git a/src/version.ts b/src/version.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0f4ec86e7cdc28b6487e85020c67ce37fc31ed5f
--- /dev/null
+++ b/src/version.ts
@@ -0,0 +1 @@
+export const APP_VERSION = "0.1.9-alpha.7";
diff --git a/yarn.lock b/yarn.lock
index 52f249918116840889f59f44288f4dfa4518983a..5f17267e42ffcfefc293b3375a840a48c7d85ade 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -955,6 +955,11 @@
   dependencies:
     "@hapi/hoek" "^8.3.0"
 
+"@hcaptcha/react-hcaptcha@^0.3.6":
+  version "0.3.6"
+  resolved "https://registry.yarnpkg.com/@hcaptcha/react-hcaptcha/-/react-hcaptcha-0.3.6.tgz#cbbb9abdaea451a4df408bc9d476e8b17f0b63f4"
+  integrity sha512-DQ5nvGVbbhd2IednxRhCV9wiPcCmclEV7bH98yGynGCXzO5XftO/XC0a1M1kEf9Ee+CLO/u+1HM/uE/PSrC3vQ==
+
 "@insertish/mutable@1.0.6":
   version "1.0.6"
   resolved "https://registry.yarnpkg.com/@insertish/mutable/-/mutable-1.0.6.tgz#f42eaba8528ff68cc8065d51f9bbbd30a24f34de"
@@ -1752,6 +1757,11 @@ define-properties@^1.1.3:
   dependencies:
     object-keys "^1.0.12"
 
+detect-browser@^5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/detect-browser/-/detect-browser-5.2.0.tgz#c9cd5afa96a6a19fda0bbe9e9be48a6b6e1e9c97"
+  integrity sha512-tr7XntDAu50BVENgQfajMLzacmSe34D+qZc4zjnniz0ZVuw/TZcLcyxHQjYpJTM36sGEkZZlYLnIM1hH7alTMA==
+
 dir-glob@^3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f"
@@ -3061,6 +3071,11 @@ react-helmet@^6.1.0:
     react-fast-compare "^3.1.1"
     react-side-effect "^2.1.0"
 
+react-hook-form@6.3.0:
+  version "6.3.0"
+  resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-6.3.0.tgz#5c1926d51d4532f44818ef73f96d1a8c11015a76"
+  integrity sha512-Xz7xxnILftxttc6H+miTSi2eYPehiW3XdsPaqY5dW8HcURFZPrnpxnmaRqz6JtZcbfRM8qjjppP/pOBaUzhn4w==
+
 react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"