diff --git a/apps/web/modules/auth/login-view.tsx b/apps/web/modules/auth/login-view.tsx index 2ea689b8373dae..b8dd5afde21e70 100644 --- a/apps/web/modules/auth/login-view.tsx +++ b/apps/web/modules/auth/login-view.tsx @@ -39,7 +39,7 @@ interface LoginValues { } const GoogleIcon = () => ( - + Continue with Google Icon ); export type PageProps = inferSSRProps; export default function Login({ @@ -195,6 +195,50 @@ PageProps & WithNonceProps<{}>) { : null }> + {!twoFactorRequired && ( + <> +
+ {isGoogleLoginEnabled && ( + + )} + {displaySSOLogin && ( + + )} +
+ {(isGoogleLoginEnabled || displaySSOLogin) && ( +
+
+
+ + {t("or").toLocaleLowerCase()} + +
+
+
+ )} + + )} +
@@ -233,7 +277,7 @@ PageProps & WithNonceProps<{}>) { {errorMessage && }
- {!twoFactorRequired && ( - <> - {(isGoogleLoginEnabled || displaySSOLogin) &&
} -
- {isGoogleLoginEnabled && ( - - )} - {displaySSOLogin && ( - - )} -
- - )} diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index c77e837262d452..cc28b2bea7876f 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -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 = async (_data) => { @@ -333,6 +336,20 @@ export default function Signup({ {/* Left side */}
+ {displayBackButton && ( +
+ +
+ )}

{IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")} @@ -347,197 +364,228 @@ export default function Signup({

)}

+ {/* Form Container */} -
-
{ - let updatedValues = values; - if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) { - updatedValues = { - ...values, - username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail), - }; - } - await signUp(updatedValues); - }}> - {/* Username */} - {!isOrgInviteByLink ? ( - 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 && ( +
+ { + let updatedValues = values; + if (!formMethods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) { + updatedValues = { + ...values, + username: getOrgUsernameFromEmail(values.email, orgAutoAcceptEmail), + }; } + await signUp(updatedValues); + }}> + {/* Username */} + {!isOrgInviteByLink ? ( + 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 */} + - ) : null} - {/* Email */} - - - {/* Password */} - - {/* Cloudflare Turnstile Captcha */} - {CLOUDFLARE_SITE_ID ? ( - { - formMethods.setValue("cfToken", token); - }} - /> - ) : null} - - handleConsentChange(COOKIE_CONSENT)} - description={t("cookie_consent_checkbox")} - /> - {errors.apiError && ( - + )} + {/* Cloudflare Turnstile Captcha */} + {CLOUDFLARE_SITE_ID ? ( + { + formMethods.setValue("cfToken", token); + }} + /> + ) : null} + + handleConsentChange(COOKIE_CONSENT)} + description={t("cookie_consent_checkbox")} /> - )} - - - {!isGoogleLoginEnabled && !isSAMLLoginEnabled ? null : ( -
-
-
- - {t("or_continue_with")} - -
-
-
- )} -
- {isGoogleLoginEnabled ? ( - + ) : ( + + )} + +
+ )} + {!displayEmailForm && ( +
+ {/* Upper Row */} +
+ {isGoogleLoginEnabled ? ( + + ) : null} +
- router.push(url); - }}> - Google - - ) : null} - {isSAMLLoginEnabled ? ( + {isGoogleLoginEnabled && ( +
+
+
+ + {t("or").toLocaleLowerCase()} + +
+
+
+ )} + + {/* Lower Row */} +
- ) : null} + {isSAMLLoginEnabled && ( + + )} +
-
+ )} + {/* Already have an account & T&C */}
diff --git a/apps/web/playwright/oidc.e2e.ts b/apps/web/playwright/oidc.e2e.ts index 0917ec22ccb80e..6cff54faed1a6b 100644 --- a/apps/web/playwright/oidc.e2e.ts +++ b/apps/web/playwright/oidc.e2e.ts @@ -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); diff --git a/apps/web/playwright/organization/organization-invitation.e2e.ts b/apps/web/playwright/organization/organization-invitation.e2e.ts index 70ab68dd458ad0..021ccc16e93303 100644 --- a/apps/web/playwright/organization/organization-invitation.e2e.ts +++ b/apps/web/playwright/organization/organization-invitation.e2e.ts @@ -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!); diff --git a/apps/web/playwright/signup.e2e.ts b/apps/web/playwright/signup.e2e.ts index 348994717c6dc7..227c9483b51424 100644 --- a/apps/web/playwright/signup.e2e.ts +++ b/apps/web/playwright/signup.e2e.ts @@ -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(); diff --git a/apps/web/public/google-icon-colored.svg b/apps/web/public/google-icon-colored.svg new file mode 100644 index 00000000000000..3f8813d5cafd37 --- /dev/null +++ b/apps/web/public/google-icon-colored.svg @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b2f7b3864d09c5..25f060ccf8dfca 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/app-store/stripepayment/lib/utils.ts b/packages/app-store/stripepayment/lib/utils.ts index 67e1dbda08e99f..3fc19527ce8260 100644 --- a/packages/app-store/stripepayment/lib/utils.ts +++ b/packages/app-store/stripepayment/lib/utils.ts @@ -18,5 +18,5 @@ export function getPerSeatPlanPrice(): string { } export function getPremiumPlanPriceValue() { - return "$29/mo"; + return "$29/month"; } diff --git a/packages/features/auth/SAMLLogin.tsx b/packages/features/auth/SAMLLogin.tsx index b5c196bf04d716..75420c5503e32d 100644 --- a/packages/features/auth/SAMLLogin.tsx +++ b/packages/features/auth/SAMLLogin.tsx @@ -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