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 <zomars@me.com>pull/2271/head
parent
1a77e4046e
commit
3341074bb2
|
@ -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<Response>): Promise<void> {
|
||||
const result = await checkPremiumUsername(req.body.username);
|
||||
return res.status(200).json(result);
|
||||
}
|
|
@ -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<typeof getServerSideProp
|
|||
/** Onboarding Steps */
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const detectStep = () => {
|
||||
// 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<typeof getServerSideProp
|
|||
token: string;
|
||||
}>({ 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<typeof getServerSideProp
|
|||
};
|
||||
});
|
||||
|
||||
mutation.mutate({
|
||||
username: usernameRef.current?.value,
|
||||
const userUpdateData = {
|
||||
name: nameRef.current?.value,
|
||||
timeZone: selectedTimeZone,
|
||||
});
|
||||
};
|
||||
|
||||
mutation.mutate(userUpdateData);
|
||||
|
||||
if (mutationComplete) {
|
||||
await mutationAsync;
|
||||
|
@ -588,7 +620,8 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
className="justify-center"
|
||||
disabled={isSubmitting}
|
||||
onClick={debouncedHandleConfirmStep}
|
||||
EndIcon={ArrowRightIcon}>
|
||||
EndIcon={ArrowRightIcon}
|
||||
data-testid={`continue-button-${currentStep}`}>
|
||||
{steps[currentStep].confirmText}
|
||||
</Button>
|
||||
</footer>
|
||||
|
@ -619,8 +652,6 @@ export default function Onboarding(props: inferSSRProps<typeof getServerSideProp
|
|||
}
|
||||
|
||||
export async function getServerSideProps(context: NextPageContext) {
|
||||
const usernameParam = asStringOrNull(context.query.username);
|
||||
|
||||
const session = await getSession(context);
|
||||
|
||||
if (!session?.user?.id) {
|
||||
|
@ -720,7 +751,6 @@ export async function getServerSideProps(context: NextPageContext) {
|
|||
connectedCalendars,
|
||||
eventTypes,
|
||||
schedules,
|
||||
usernameParam,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,8 +1,24 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import prisma from "@lib/prisma";
|
||||
|
||||
test.describe("Onboarding", () => {
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<ResponseUsernameApi> {
|
||||
const username = slugify(_username);
|
||||
const response = await fetch("https://cal.com/api/username", {
|
||||
credentials: "include",
|
||||
|
|
Loading…
Reference in New Issue