feat: New UI for SignUp / Login (#16802)

This commit is contained in:
Benny Joo
2024-09-26 15:27:56 -04:00
committed by GitHub
parent ffa58d1cdb
commit 90f6c7e239
9 changed files with 343 additions and 221 deletions
+46 -34
View File
@@ -39,7 +39,7 @@ interface LoginValues {
}
const GoogleIcon = () => (
<img className="text-subtle mr-2 h-4 w-4 dark:invert" src="/google-icon.svg" alt="" />
<img className="text-subtle mr-2 h-4 w-4" src="/google-icon-colored.svg" alt="Continue with Google Icon" />
);
export type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Login({
@@ -195,6 +195,50 @@ PageProps & WithNonceProps<{}>) {
: null
}>
<FormProvider {...methods}>
{!twoFactorRequired && (
<>
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="primary"
className="w-full justify-center"
disabled={formState.isSubmitting}
data-testid="google"
CustomStartIcon={<GoogleIcon />}
onClick={async (e) => {
e.preventDefault();
setLastUsed("google");
await signIn("google", {
callbackUrl,
});
}}>
<span>{t("signin_with_google")}</span>
{lastUsed === "google" && <LastUsed />}
</Button>
)}
{displaySSOLogin && (
<SAMLLogin
disabled={formState.isSubmitting}
samlTenantID={samlTenantID}
samlProductID={samlProductID}
setErrorMessage={setErrorMessage}
/>
)}
</div>
{(isGoogleLoginEnabled || displaySSOLogin) && (
<div className="my-8">
<div className="relative flex items-center">
<div className="border-subtle flex-grow border-t" />
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
{t("or").toLocaleLowerCase()}
</span>
<div className="border-subtle flex-grow border-t" />
</div>
</div>
)}
</>
)}
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate data-testid="login-form">
<div>
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...register("csrfToken")} />
@@ -233,7 +277,7 @@ PageProps & WithNonceProps<{}>) {
{errorMessage && <Alert severity="error" title={errorMessage} />}
<Button
type="submit"
color="primary"
color="secondary"
disabled={formState.isSubmitting}
className="w-full justify-center">
<span>{twoFactorRequired ? t("submit") : t("sign_in")}</span>
@@ -243,38 +287,6 @@ PageProps & WithNonceProps<{}>) {
</Button>
</div>
</form>
{!twoFactorRequired && (
<>
{(isGoogleLoginEnabled || displaySSOLogin) && <hr className="border-subtle my-8" />}
<div className="space-y-3">
{isGoogleLoginEnabled && (
<Button
color="secondary"
className="w-full justify-center"
disabled={formState.isSubmitting}
data-testid="google"
CustomStartIcon={<GoogleIcon />}
onClick={async (e) => {
e.preventDefault();
setLastUsed("google");
await signIn("google", {
callbackUrl,
});
}}>
<span>{t("signin_with_google")}</span>
{lastUsed === "google" && <LastUsed />}
</Button>
)}
{displaySSOLogin && (
<SAMLLogin
samlTenantID={samlTenantID}
samlProductID={samlProductID}
setErrorMessage={setErrorMessage}
/>
)}
</div>
</>
)}
</FormProvider>
</AuthContainer>
<AddToHomescreen />
+227 -179
View File
@@ -46,9 +46,9 @@ import {
TextField,
Form,
Alert,
showToast,
CheckboxField,
Icon,
showToast,
} from "@calcom/ui";
import type { getServerSideProps } from "@lib/signup/getServerSideProps";
@@ -179,9 +179,12 @@ export default function Signup({
redirectUrl,
emailVerificationEnabled,
}: SignupProps) {
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
const [isSamlSignup, setIsSamlSignup] = useState(false);
const [premiumUsername, setPremiumUsername] = useState(false);
const [usernameTaken, setUsernameTaken] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [displayEmailForm, setDisplayEmailForm] = useState(token);
const searchParams = useCompatSearchParams();
const telemetry = useTelemetry();
const { t, i18n } = useLocale();
@@ -210,6 +213,7 @@ export default function Signup({
}
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
const displayBackButton = token ? false : displayEmailForm;
const handleErrorsAndStripe = async (resp: Response) => {
if (!resp.ok) {
@@ -229,7 +233,6 @@ export default function Signup({
}
};
const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new");
const signUp: SubmitHandler<FormValues> = async (_data) => {
@@ -333,6 +336,20 @@ export default function Signup({
<HeadSeo title={t("sign_up")} description={t("sign_up")} />
{/* Left side */}
<div className="ml-auto mr-auto mt-0 flex w-full max-w-xl flex-col px-4 pt-6 sm:px-16 md:px-20 lg:mt-12 2xl:px-28">
{displayBackButton && (
<div className="flex w-fit lg:-mt-12">
<Button
color="minimal"
className="hover:bg-subtle todesktop:mt-10 mb-6 flex h-6 max-h-6 w-full items-center rounded-md px-3 py-2"
StartIcon="arrow-left"
onClick={() => {
setDisplayEmailForm(false);
setIsSamlSignup(false);
}}>
{t("back")}
</Button>
</div>
)}
<div className="flex flex-col gap-2">
<h1 className="font-cal text-[28px] leading-none ">
{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
@@ -347,197 +364,228 @@ export default function Signup({
</p>
)}
</div>
{/* Form Container */}
<div className="mt-12">
<Form
className="flex flex-col gap-4"
form={formMethods}
handleSubmit={async (values) => {
let updatedValues = values;
if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
updatedValues = {
...values,
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail),
};
}
await signUp(updatedValues);
}}>
{/* Username */}
{!isOrgInviteByLink ? (
<UsernameField
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
usernameTaken={usernameTaken}
disabled={!!orgSlug}
setUsernameTaken={(value) => setUsernameTaken(value)}
data-testid="signup-usernamefield"
setPremium={(value) => setPremiumUsername(value)}
addOnLeading={
orgSlug
? `${getOrgFullOrigin(orgSlug, { protocol: true }).replace(URL_PROTOCOL_REGEX, "")}/`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`
{displayEmailForm && (
<div className="mt-12">
<Form
className="flex flex-col gap-4"
form={formMethods}
handleSubmit={async (values) => {
let updatedValues = values;
if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
updatedValues = {
...values,
username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail),
};
}
await signUp(updatedValues);
}}>
{/* Username */}
{!isOrgInviteByLink ? (
<UsernameField
orgSlug={orgSlug}
label={t("username")}
username={watch("username") || ""}
premium={premiumUsername}
usernameTaken={usernameTaken}
disabled={!!orgSlug}
setUsernameTaken={(value) => setUsernameTaken(value)}
data-testid="signup-usernamefield"
setPremium={(value) => setPremiumUsername(value)}
addOnLeading={
orgSlug
? `${getOrgFullOrigin(orgSlug, { protocol: true }).replace(
URL_PROTOCOL_REGEX,
""
)}/`
: `${process.env.NEXT_PUBLIC_WEBSITE_URL.replace(URL_PROTOCOL_REGEX, "")}/`
}
/>
) : null}
{/* Email */}
<TextField
{...register("email")}
label={t("email")}
type="email"
disabled={prepopulateFormValues?.email}
data-testid="signup-emailfield"
/>
) : null}
{/* Email */}
<TextField
{...register("email")}
label={t("email")}
type="email"
disabled={prepopulateFormValues?.email}
data-testid="signup-emailfield"
/>
{/* Password */}
<PasswordField
data-testid="signup-passwordfield"
label={t("password")}
{...register("password")}
hintErrors={["caplow", "min", "num"]}
/>
{/* Cloudflare Turnstile Captcha */}
{CLOUDFLARE_SITE_ID ? (
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
formMethods.setValue("cfToken", token);
}}
/>
) : null}
{/* Password */}
{!isSamlSignup && (
<PasswordField
data-testid="signup-passwordfield"
label={t("password")}
{...register("password")}
hintErrors={["caplow", "min", "num"]}
/>
)}
{/* Cloudflare Turnstile Captcha */}
{CLOUDFLARE_SITE_ID ? (
<TurnstileCaptcha
appearance="interaction-only"
onVerify={(token) => {
formMethods.setValue("cfToken", token);
}}
/>
) : null}
<CheckboxField
onChange={() => handleConsentChange(COOKIE_CONSENT)}
description={t("cookie_consent_checkbox")}
/>
{errors.apiError && (
<Alert
className="mb-3"
severity="error"
message={errors.apiError?.message}
data-testid="signup-error-message"
<CheckboxField
onChange={() => handleConsentChange(COOKIE_CONSENT)}
description={t("cookie_consent_checkbox")}
/>
)}
<Button
type="submit"
className="my-2 w-full justify-center"
loading={loadingSubmitState}
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
!formMethods.getValues("email") ||
!formMethods.getValues("password") ||
(CLOUDFLARE_SITE_ID &&
!process.env.NEXT_PUBLIC_IS_E2E &&
!formMethods.getValues("cfToken")) ||
isSubmitting ||
usernameTaken
}>
{premiumUsername && !usernameTaken
? `Create Account for ${getPremiumPlanPriceValue()}`
: t("create_account")}
</Button>
</Form>
{!isGoogleLoginEnabled && !isSAMLLoginEnabled ? null : (
<div className="mt-6">
<div className="relative flex items-center">
<div className="border-subtle flex-grow border-t" />
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
{t("or_continue_with")}
</span>
<div className="border-subtle flex-grow border-t" />
</div>
</div>
)}
<div className="mt-6 flex flex-col gap-2 md:flex-row">
{isGoogleLoginEnabled ? (
<Button
color="secondary"
disabled={!!formMethods.formState.errors.username || premiumUsername}
loading={isGoogleLoading}
CustomStartIcon={
<img
className={classNames(
"text-subtle mr-2 h-4 w-4 dark:invert",
premiumUsername && "opacity-50"
)}
src="/google-icon.svg"
alt=""
/>
}
className={classNames(
"w-full justify-center rounded-md text-center",
formMethods.formState.errors.username ? "opacity-50" : ""
)}
onClick={async () => {
setIsGoogleLoading(true);
const username = formMethods.getValues("username");
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
const searchQueryParams = new URLSearchParams();
if (username) {
// If username is present we save it in query params to check for premium
searchQueryParams.set("username", username);
{errors.apiError && (
<Alert
className="mb-3"
severity="error"
message={errors.apiError?.message}
data-testid="signup-error-message"
/>
)}
{isSamlSignup ? (
<Button
color="primary"
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
!formMethods.getValues("email") ||
!formMethods.getValues("username") ||
premiumUsername ||
isSubmitting
}
onClick={() => {
const username = formMethods.getValues("username");
if (!username) {
showToast("error", t("username_required"));
return;
}
localStorage.setItem("username", username);
const sp = new URLSearchParams();
// @NOTE: don't remove username query param as it's required right now for stripe payment page
sp.set("username", username);
sp.set("email", formMethods.getValues("email"));
router.push(
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
);
}}
className={classNames(
"my-2 w-full justify-center rounded-md text-center",
formMethods.formState.errors.username && formMethods.formState.errors.email
? "opacity-50"
: ""
)}>
<Icon name="shield-check" className="mr-2 h-5 w-5" />
{t("create_account_with_saml")}
</Button>
) : (
<Button
type="submit"
data-testid="signup-submit-button"
className="my-2 w-full justify-center"
loading={loadingSubmitState}
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
!formMethods.getValues("email") ||
!formMethods.getValues("password") ||
(CLOUDFLARE_SITE_ID &&
!process.env.NEXT_PUBLIC_IS_E2E &&
!formMethods.getValues("cfToken")) ||
isSubmitting ||
usernameTaken
}>
{premiumUsername && !usernameTaken
? `${t("create_account")} (${getPremiumPlanPriceValue()})`
: t("create_account")}
</Button>
)}
</Form>
</div>
)}
{!displayEmailForm && (
<div className="mt-24">
{/* Upper Row */}
<div className="mt-6 flex flex-col gap-2 md:flex-row">
{isGoogleLoginEnabled ? (
<Button
color="primary"
loading={isGoogleLoading}
CustomStartIcon={
<img
className={classNames("text-subtle mr-2 h-4 w-4", premiumUsername && "opacity-50")}
src="/google-icon-colored.svg"
alt="Continue with Google Icon"
/>
}
if (token) {
searchQueryParams.set("email", prepopulateFormValues?.email);
}
const url = searchQueryParams.toString()
? `${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`
: GOOGLE_AUTH_URL;
className={classNames("w-full justify-center rounded-md text-center")}
data-testid="continue-with-google-button"
onClick={async () => {
setIsSamlSignup(false);
setIsGoogleLoading(true);
const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
const searchQueryParams = new URLSearchParams();
if (prepopulateFormValues?.username) {
// If username is present we save it in query params to check for premium
searchQueryParams.set("username", prepopulateFormValues.username);
localStorage.setItem("username", prepopulateFormValues.username);
}
if (token) {
searchQueryParams.set("email", prepopulateFormValues?.email);
}
const url = searchQueryParams.toString()
? `${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`
: GOOGLE_AUTH_URL;
router.push(url);
}}>
Google
</Button>
) : null}
{isSAMLLoginEnabled ? (
router.push(url);
}}>
{t("continue_with_google")}
</Button>
) : null}
</div>
{isGoogleLoginEnabled && (
<div className="mt-6">
<div className="relative flex items-center">
<div className="border-subtle flex-grow border-t" />
<span className="text-subtle mx-2 flex-shrink text-sm font-normal leading-none">
{t("or").toLocaleLowerCase()}
</span>
<div className="border-subtle flex-grow border-t" />
</div>
</div>
)}
{/* Lower Row */}
<div className="mt-6 flex flex-col gap-2">
<Button
color="secondary"
disabled={
!!formMethods.formState.errors.username ||
!!formMethods.formState.errors.email ||
premiumUsername ||
isSubmitting ||
isGoogleLoading
}
className={classNames(
"w-full justify-center rounded-md text-center",
formMethods.formState.errors.username && formMethods.formState.errors.email
? "opacity-50"
: ""
)}
disabled={isGoogleLoading}
className={classNames("w-full justify-center rounded-md text-center")}
onClick={() => {
if (!formMethods.getValues("username")) {
formMethods.trigger("username");
}
if (!formMethods.getValues("email")) {
formMethods.trigger("email");
return;
}
const username = formMethods.getValues("username");
if (!username) {
showToast("error", t("username_required"));
return;
}
localStorage.setItem("username", username);
const sp = new URLSearchParams();
// @NOTE: don't remove username query param as it's required right now for stripe payment page
sp.set("username", username);
sp.set("email", formMethods.getValues("email"));
router.push(
`${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
);
}}>
<Icon name="shield-check" className="mr-2 h-5 w-5" />
{t("saml_sso")}
setDisplayEmailForm(true);
setIsSamlSignup(false);
}}
data-testid="continue-with-email-button">
{t("continue_with_email")}
</Button>
) : null}
{isSAMLLoginEnabled && (
<Button
color="minimal"
disabled={isGoogleLoading}
className={classNames("w-full justify-center rounded-md text-center")}
onClick={() => {
setDisplayEmailForm(true);
setIsSamlSignup(true);
}}>
{`${t("or").toLocaleLowerCase()} ${t("saml_sso")}`}
</Button>
)}
</div>
</div>
</div>
)}
{/* Already have an account & T&C */}
<div className="mt-10 flex h-full flex-col justify-end pb-6 text-xs">
<div className="flex flex-col text-sm">
+1 -1
View File
@@ -49,7 +49,7 @@ test.describe("OIDC", () => {
// Login a user using the OIDC provider.
// The credentials are handled by the provider, so we don't need to create a user in the db.
await page.goto("/auth/login");
await page.click('[data-testid="saml"]');
await page.click('[data-testid="samlAndOidc"]');
// Redirected outide of the app, the user would be redirected to the OIDC provider.
await page.waitForURL(/https:\/\/[^/]+\/oauth2\/v1\/authorize\?.*/);
await page.getByRole("textbox", { name: "Username" }).fill(OIDC_USER_EMAIL);
@@ -297,6 +297,9 @@ test.describe("Organization", () => {
await test.step("Signing up with the previous username of the migrated user - shouldn't be allowed", async () => {
await page.goto("/signup");
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await page.locator('input[name="username"]').fill(existingUser.username!);
+42 -3
View File
@@ -10,7 +10,26 @@ import { expectInvitationEmailToBeReceived } from "./team/expects";
test.describe.configure({ mode: "parallel" });
test.describe("Signup Flow Test", async () => {
test.describe("Signup Main Page Test", async () => {
test.beforeEach(async ({ features }) => {
features.reset();
});
test("Continue with email button must exist / work", async ({ page }) => {
await page.goto("/signup");
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
});
test("Continue with google button must exist / work", async ({ page }) => {
await page.goto("/signup");
await expect(page.locator('[data-testid="continue-with-google-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-google-button"]').click();
await page.waitForURL("/auth/sso/google");
});
});
test.describe("Email Signup Flow Test", async () => {
test.beforeEach(async ({ features }) => {
features.reset(); // This resets to the inital state not an empt yarray
});
@@ -26,6 +45,9 @@ test.describe("Signup Flow Test", async () => {
await page.goto("/signup");
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
const alertMessage = "Username or email is already taken";
@@ -52,8 +74,10 @@ test.describe("Signup Flow Test", async () => {
});
await page.goto("/signup");
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
const alertMessage = "Username or email is already taken";
@@ -85,6 +109,9 @@ test.describe("Signup Flow Test", async () => {
// Signup with premium username name
await page.goto("/signup");
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
// Fill form
await page.locator('input[name="username"]').fill("rock");
@@ -113,6 +140,9 @@ test.describe("Signup Flow Test", async () => {
await page.goto("/signup");
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
// Fill form
await page.locator('input[name="username"]').fill(userToCreate.username);
@@ -132,6 +162,9 @@ test.describe("Signup Flow Test", async () => {
const signupUrlWithParams = "/signup?username=rick-jones&email=rick-jones%40example.com";
await page.goto(signupUrlWithParams);
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
// Fill form
const usernameInput = page.locator('input[name="username"]');
@@ -182,6 +215,7 @@ test.describe("Signup Flow Test", async () => {
const signupUrlWithToken = `/signup?token=${token}`;
await page.goto(signupUrlWithToken);
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
const usernameField = page.locator('input[name="username"]');
const emailField = page.locator('input[name="email"]');
@@ -213,6 +247,9 @@ test.describe("Signup Flow Test", async () => {
await page.goto("/signup");
await expect(page.locator("text=Create your account")).toBeVisible();
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
// Fill form
await page.locator('input[name="username"]').fill(userToCreate.username);
@@ -283,7 +320,9 @@ test.describe("Signup Flow Test", async () => {
const url = new URL(newPage.url());
expect(url.pathname).toBe("/signup");
await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible();
await page.locator('[data-testid="continue-with-email-button"]').click();
await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible();
// Check required fields
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await newPage.locator("button[type=submit]").click();
+10
View File
@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0 0 48 48">
<path fill="#FFC107"
d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"></path>
<path fill="#FF3D00"
d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"></path>
<path fill="#4CAF50"
d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"></path>
<path fill="#1976D2"
d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -972,6 +972,8 @@
"signin_with_google": "Sign in with Google",
"signin_with_saml": "Sign in with SAML",
"signin_with_saml_oidc": "Sign in with SAML/OIDC",
"continue_with_email": "Continue with email",
"continue_with_google": "Continue with Google",
"last_used": "Last used",
"you_will_need_to_generate": "You will need to generate an access token from your old scheduling tool.",
"import": "Import",
@@ -2484,6 +2486,7 @@
"lock_org_users_eventtypes_description": "Prevent members from creating their own event types.",
"add_to_event_type": "Add to event type",
"create_account_password": "Create account password",
"create_account_with_saml": "Create Account with SAML",
"error_creating_account_password": "Failed to create account password",
"cannot_create_account_password_cal_provider": "Cannot create account password for cal accounts",
"cannot_create_account_password_already_existing": "Cannot create account password for already created ones",
@@ -18,5 +18,5 @@ export function getPerSeatPlanPrice(): string {
}
export function getPremiumPlanPriceValue() {
return "$29/mo";
return "$29/month";
}
+10 -3
View File
@@ -7,6 +7,7 @@ import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
import { LastUsed, useLastUsed } from "@calcom/lib/hooks/useLastUsed";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { ButtonProps } from "@calcom/ui";
import { Button } from "@calcom/ui";
interface Props {
@@ -19,7 +20,12 @@ const schema = z.object({
email: z.string().email({ message: "Please enter a valid email" }),
});
export function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Props) {
export function SAMLLogin({
samlTenantID,
samlProductID,
setErrorMessage,
...buttonProps
}: Props & ButtonProps) {
const { t } = useLocale();
const methods = useFormContext();
const [lastUsed, setLastUsed] = useLastUsed();
@@ -38,7 +44,7 @@ export function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Prop
<Button
StartIcon="lock"
color="secondary"
data-testid="saml"
data-testid="samlAndOidc"
className="flex w-full justify-center"
onClick={async (event) => {
event.preventDefault();
@@ -64,7 +70,8 @@ export function SAMLLogin({ samlTenantID, samlProductID, setErrorMessage }: Prop
mutation.mutate({
email,
});
}}>
}}
{...buttonProps}>
<span>{t("signin_with_saml_oidc")}</span>
{lastUsed === "saml" && <LastUsed />}
</Button>