Feat/3796 new UI for signin (#4369)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
sean-brydon
kodiakhq[bot]
parent
6c4a372121
commit
404168fe7d
@@ -1,7 +1,8 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { ComponentMeta, ComponentStory } from "@storybook/react";
|
||||
import { Copy } from "react-feather";
|
||||
|
||||
import { TextAreaField, TextField } from "@calcom/ui/v2/core/form/fields";
|
||||
import { TextAreaField, TextField, PasswordField } from "@calcom/ui/v2/core/form/fields";
|
||||
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||
|
||||
export default {
|
||||
@@ -66,3 +67,9 @@ export const TextAreaInput: ComponentStory<typeof TextAreaField> = () => (
|
||||
);
|
||||
|
||||
export const DatePickerInput: ComponentStory<typeof DatePicker> = () => <DatePicker date={new Date()} />;
|
||||
|
||||
export const PasswordInput: ComponentStory<typeof PasswordField> = () => (
|
||||
<TooltipProvider>
|
||||
<PasswordField />
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
@@ -32,10 +32,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-auto mt-8 sm:mx-auto sm:w-full sm:max-w-md">
|
||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-8 sm:px-10">
|
||||
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="mt-4 text-center text-sm text-neutral-600">{props.footerText}</div>
|
||||
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -18,6 +18,7 @@ const V2_WHITELIST = [
|
||||
"/workflows",
|
||||
"/apps",
|
||||
"/success",
|
||||
"/auth/login",
|
||||
];
|
||||
const V2_BLACKLIST = [
|
||||
//
|
||||
|
||||
@@ -5,13 +5,14 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FaGoogle } from "react-icons/fa";
|
||||
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Button, EmailField, Form, PasswordField } from "@calcom/ui/v2";
|
||||
import SAMLLogin from "@calcom/ui/v2/modules/auth/SAMLLogin";
|
||||
|
||||
@@ -22,7 +23,7 @@ import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AddToHomescreen from "@components/AddToHomescreen";
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
import AuthContainer from "@components/v2/ui/AuthContainer";
|
||||
|
||||
import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
@@ -76,12 +77,9 @@ export default function Login({
|
||||
callbackUrl = safeCallbackUrl || "";
|
||||
|
||||
const LoginFooter = (
|
||||
<span className="text-gray-600">
|
||||
{t("dont_have_an_account")}{" "}
|
||||
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
|
||||
{t("create_an_account")}
|
||||
</a>
|
||||
</span>
|
||||
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
|
||||
{t("i_dont_have_an_account")}
|
||||
</a>
|
||||
);
|
||||
|
||||
const TwoFactorFooter = (
|
||||
@@ -106,7 +104,6 @@ export default function Login({
|
||||
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
|
||||
<Form
|
||||
form={form}
|
||||
className="space-y-6"
|
||||
handleSubmit={async (values) => {
|
||||
setErrorMessage(null);
|
||||
telemetry.event(telemetryEventTypes.login, collectPageParameters());
|
||||
@@ -132,51 +129,52 @@ export default function Login({
|
||||
{...form.register("csrfToken")}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
|
||||
<EmailField
|
||||
id="email"
|
||||
label={t("email_address")}
|
||||
defaultValue={router.query.email as string}
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
{...form.register("email")}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="absolute right-0 -top-[2px]">
|
||||
<Link href="/auth/forgot-password">
|
||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
||||
{t("forgot_password")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<PasswordField
|
||||
id="password"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
<div className="space-y-6">
|
||||
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
|
||||
<EmailField
|
||||
id="email"
|
||||
label={t("email_address")}
|
||||
defaultValue={router.query.email as string}
|
||||
placeholder="john.doe@example.com"
|
||||
required
|
||||
{...form.register("password")}
|
||||
{...form.register("email")}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="absolute right-0 -top-[6px]">
|
||||
<Link href="/auth/forgot-password">
|
||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
||||
{t("forgot")}
|
||||
</a>
|
||||
</Link>
|
||||
</div>
|
||||
<PasswordField
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
required
|
||||
className="mb-0"
|
||||
{...form.register("password")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{twoFactorRequired && <TwoFactor center />}
|
||||
{twoFactorRequired && <TwoFactor center />}
|
||||
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||
<div className="pb-8">
|
||||
<Button type="submit" color="primary" disabled={isSubmitting} className="w-full">
|
||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||
<Button type="submit" color="primary" disabled={isSubmitting} className="w-full justify-center">
|
||||
{twoFactorRequired ? t("submit") : t("sign_in")}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
<hr />
|
||||
{!twoFactorRequired && (
|
||||
<>
|
||||
{isGoogleLoginEnabled && (
|
||||
<div className="mt-8">
|
||||
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
|
||||
<div className="space-y-3">
|
||||
{isGoogleLoginEnabled && (
|
||||
<Button
|
||||
color="secondary"
|
||||
className="w-full"
|
||||
className="w-full justify-center"
|
||||
data-testid="google"
|
||||
StartIcon={FaGoogle}
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
// track Google logins. Without personal data/payload
|
||||
@@ -185,17 +183,17 @@ export default function Login({
|
||||
}}>
|
||||
{t("signin_with_google")}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{isSAMLLoginEnabled && (
|
||||
<SAMLLogin
|
||||
email={form.getValues("email")}
|
||||
samlTenantID={samlTenantID}
|
||||
samlProductID={samlProductID}
|
||||
hostedCal={hostedCal}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
)}
|
||||
{isSAMLLoginEnabled && (
|
||||
<SAMLLogin
|
||||
email={form.getValues("email")}
|
||||
samlTenantID={samlTenantID}
|
||||
samlProductID={samlProductID}
|
||||
hostedCal={hostedCal}
|
||||
setErrorMessage={setErrorMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</AuthContainer>
|
||||
|
||||
@@ -206,6 +206,7 @@
|
||||
"2fa_enter_six_digit_code": "Enter the six-digit code from your authenticator app below.",
|
||||
"create_an_account": "Create an account",
|
||||
"dont_have_an_account": "Don't have an account?",
|
||||
"i_dont_have_an_account": "I don't have an account",
|
||||
"2fa_code": "Two-Factor Code",
|
||||
"sign_in_account": "Sign in to your account",
|
||||
"sign_in": "Sign in",
|
||||
@@ -266,6 +267,8 @@
|
||||
"enter_new_password": "Enter the new password you'd like for your account.",
|
||||
"reset_password": "Reset Password",
|
||||
"change_your_password": "Change your password",
|
||||
"show_password": "Show password",
|
||||
"hide_password": "Hide password",
|
||||
"try_again": "Try Again",
|
||||
"request_is_expired": "That Request is Expired.",
|
||||
"reset_instructions": "Enter the email address associated with your account and we will send you a link to reset your password.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import React, { forwardRef, ReactElement, ReactNode, Ref } from "react";
|
||||
import { Check, Circle, Info, X } from "react-feather";
|
||||
import React, { forwardRef, ReactElement, ReactNode, Ref, useCallback, useMemo, useState } from "react";
|
||||
import { Check, Circle, Info, X, Eye, EyeOff } from "react-feather";
|
||||
import {
|
||||
FieldErrors,
|
||||
FieldValues,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Tooltip } from "@calcom/ui/v2";
|
||||
import showToast from "@calcom/ui/v2/core/notifications";
|
||||
|
||||
import { Alert } from "../../../Alert";
|
||||
@@ -248,7 +249,39 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return <InputField type="password" placeholder="•••••••••••••" ref={ref} {...props} />;
|
||||
const { t } = useLocale();
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const toggleIsPasswordVisible = useCallback(
|
||||
() => setIsPasswordVisible(!isPasswordVisible),
|
||||
[isPasswordVisible, setIsPasswordVisible]
|
||||
);
|
||||
const textLabel = isPasswordVisible ? t("hide_password") : t("show_password");
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<InputField
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
placeholder="•••••••••••••"
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classNames("pr-10", props.className)}
|
||||
/>
|
||||
|
||||
<Tooltip content={textLabel}>
|
||||
<button
|
||||
className="absolute bottom-0 right-3 h-9 text-gray-900"
|
||||
type="button"
|
||||
onClick={() => toggleIsPasswordVisible()}>
|
||||
{isPasswordVisible ? (
|
||||
<EyeOff className="h-4 stroke-[2.5px]" />
|
||||
) : (
|
||||
<Eye className="h-4 stroke-[2.5px]" />
|
||||
)}
|
||||
<span className="sr-only">{textLabel}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const EmailInput = forwardRef<HTMLInputElement, InputFieldProps>(function EmailInput(props, ref) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useFormContext } from "react-hook-form";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
|
||||
interface Props {
|
||||
@@ -30,32 +31,31 @@ export default function SAMLLogin(props: Props) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-5">
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="saml"
|
||||
className="flex w-full justify-center"
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
<Button
|
||||
StartIcon={Icon.FiLock}
|
||||
color="secondary"
|
||||
data-testid="saml"
|
||||
className="flex w-full justify-center"
|
||||
onClick={async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
// track Google logins. Without personal data/payload
|
||||
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
|
||||
// track Google logins. Without personal data/payload
|
||||
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
|
||||
|
||||
if (!props.hostedCal) {
|
||||
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
|
||||
} else {
|
||||
if (props.email.length === 0) {
|
||||
props.setErrorMessage(t("saml_email_required"));
|
||||
return;
|
||||
}
|
||||
// hosted solution, fetch tenant and product from the backend
|
||||
mutation.mutate({
|
||||
email: methods.getValues("email"),
|
||||
});
|
||||
if (!props.hostedCal) {
|
||||
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
|
||||
} else {
|
||||
if (props.email.length === 0) {
|
||||
props.setErrorMessage(t("saml_email_required"));
|
||||
return;
|
||||
}
|
||||
}}>
|
||||
{t("signin_with_saml")}
|
||||
</Button>
|
||||
</div>
|
||||
// hosted solution, fetch tenant and product from the backend
|
||||
mutation.mutate({
|
||||
email: methods.getValues("email"),
|
||||
});
|
||||
}
|
||||
}}>
|
||||
{t("signin_with_saml")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user