From 3341074bb2099a6c01f9bd842cf9fe9bd0d62906 Mon Sep 17 00:00:00 2001 From: alannnc Date: Thu, 24 Mar 2022 10:45:56 -0700 Subject: [PATCH] Fix/login username registration (#2241) * username update from getting-started when received as query param * Added test for onboarding username update * Now saving username saved in localStorage * remove username field * Removed wordlist * Implement checkoutUsername as api endpoint * Remove unused lib utils not empty Co-authored-by: zomars --- apps/web/pages/api/username.ts | 13 ++++ apps/web/pages/getting-started.tsx | 64 ++++++++++++++------ apps/web/playwright/onboarding.test.ts | 35 +++++++++++ packages/ee/lib/core/checkPremiumUsername.ts | 6 +- 4 files changed, 99 insertions(+), 19 deletions(-) create mode 100644 apps/web/pages/api/username.ts diff --git a/apps/web/pages/api/username.ts b/apps/web/pages/api/username.ts new file mode 100644 index 00000000..e1fd6d91 --- /dev/null +++ b/apps/web/pages/api/username.ts @@ -0,0 +1,13 @@ +import type { NextApiRequest, NextApiResponse } from "next"; + +import { checkPremiumUsername } from "@calcom/ee/lib/core/checkPremiumUsername"; + +type Response = { + available: boolean; + premium: boolean; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse): Promise { + const result = await checkPremiumUsername(req.body.username); + return res.status(200).json(result); +} diff --git a/apps/web/pages/getting-started.tsx b/apps/web/pages/getting-started.tsx index 0ef6f8c1..a5c9db8a 100644 --- a/apps/web/pages/getting-started.tsx +++ b/apps/web/pages/getting-started.tsx @@ -1,6 +1,6 @@ import { ArrowRightIcon } from "@heroicons/react/outline"; import { zodResolver } from "@hookform/resolvers/zod"; -import { IdentityProvider, Prisma } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import classnames from "classnames"; import dayjs from "dayjs"; import localizedFormat from "dayjs/plugin/localizedFormat"; @@ -19,11 +19,11 @@ import * as z from "zod"; import getApps from "@calcom/app-store/utils"; import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager"; +import { ResponseUsernameApi } from "@calcom/ee/lib/core/checkPremiumUsername"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import { Form } from "@calcom/ui/form/fields"; -import { asStringOrNull } from "@lib/asStringOrNull"; import { getSession } from "@lib/auth"; import { DEFAULT_SCHEDULE } from "@lib/availability"; import { useLocale } from "@lib/hooks/useLocale"; @@ -152,15 +152,8 @@ export default function Onboarding(props: inferSSRProps { + // Always set timezone if new user let step = 0; - const hasSetUserNameOrTimeZone = - props.user?.name && - props.user?.timeZone && - !props.usernameParam && - props.user?.identityProvider === IdentityProvider.CAL; - if (hasSetUserNameOrTimeZone) { - step = 1; - } const hasConfigureCalendar = props.integrations.some((integration) => integration.credential !== null); if (hasConfigureCalendar) { @@ -259,6 +252,44 @@ export default function Onboarding(props: inferSSRProps({ resolver: zodResolver(schema), mode: "onSubmit" }); + const fetchUsername = async (username: string) => { + const response = await fetch(`${process.env.NEXT_PUBLIC_APP_URL}/api/username`, { + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ username: username.trim() }), + method: "POST", + mode: "cors", + }); + const data = (await response.json()) as ResponseUsernameApi; + return { response, data }; + }; + + // Should update username on user when being redirected from sign up and doing google/saml + useEffect(() => { + async function validateAndSave(username) { + const { data } = await fetchUsername(username); + + // Only persist username if its available and not premium + // premium usernames are saved via stripe webhook + if (data.available && !data.premium) { + await updateUser({ + username, + }); + } + // Remove it from localStorage + window.localStorage.removeItem("username"); + return; + } + + // Looking for username on localStorage + const username = window.localStorage.getItem("username"); + if (username) { + validateAndSave(username); + } + }, []); + const availabilityForm = useForm({ defaultValues: { schedule: DEFAULT_SCHEDULE } }); const steps = [ { @@ -398,11 +429,12 @@ export default function Onboarding(props: inferSSRProps + EndIcon={ArrowRightIcon} + data-testid={`continue-button-${currentStep}`}> {steps[currentStep].confirmText} @@ -619,8 +652,6 @@ export default function Onboarding(props: inferSSRProps { test.use({ storageState: "playwright/artifacts/onboardingStorageState.json" }); + // You want to always reset account completedOnboarding after each test + test.afterEach(async () => { + // Revert DB change + await prisma.user.update({ + where: { + email: "onboarding@example.com", + }, + data: { + username: "onboarding", + completedOnboarding: false, + }, + }); + }); + test("redirects to /getting-started after login", async ({ page }) => { await page.goto("/event-types"); await page.waitForNavigation({ @@ -11,4 +27,23 @@ test.describe("Onboarding", () => { }, }); }); + + test.describe("Onboarding", () => { + test("update onboarding username via localstorage", async ({ page }) => { + await page.addInitScript(() => { + window.localStorage.setItem("username", "alwaysavailable"); + }, {}); + // Try to go getting started with a available username + await page.goto("/getting-started"); + // Wait for useEffectUpdate to run + await page.waitForTimeout(1000); + + const updatedUser = await prisma.user.findUnique({ + where: { email: "onboarding@example.com" }, + select: { id: true, username: true }, + }); + + expect(updatedUser?.username).toBe("alwaysavailable"); + }); + }); }); diff --git a/packages/ee/lib/core/checkPremiumUsername.ts b/packages/ee/lib/core/checkPremiumUsername.ts index 5a000946..25df1108 100644 --- a/packages/ee/lib/core/checkPremiumUsername.ts +++ b/packages/ee/lib/core/checkPremiumUsername.ts @@ -1,11 +1,13 @@ import slugify from "@calcom/lib/slugify"; -export async function checkPremiumUsername(_username: string): Promise<{ +export type ResponseUsernameApi = { available: boolean; premium: boolean; message?: string; suggestion?: string; -}> { +}; + +export async function checkPremiumUsername(_username: string): Promise { const username = slugify(_username); const response = await fetch("https://cal.com/api/username", { credentials: "include",