From f2362eda322704a366cf54e56c29f7c2fb26be96 Mon Sep 17 00:00:00 2001 From: Alex van Andel Date: Sun, 31 Oct 2021 23:01:46 +0100 Subject: [PATCH 1/5] Fixed #1015 - Teams user registration is broken --- components/form/fields.tsx | 71 ++++++++--- components/ui/UsernameInput.tsx | 3 + pages/api/auth/signup.ts | 7 ++ pages/auth/signup.tsx | 177 +++++++++++++-------------- public/static/locales/en/common.json | 5 +- 5 files changed, 159 insertions(+), 104 deletions(-) diff --git a/components/form/fields.tsx b/components/form/fields.tsx index f4b27cc8c7975..e6bbc8aeb8380 100644 --- a/components/form/fields.tsx +++ b/components/form/fields.tsx @@ -1,8 +1,11 @@ import { useId } from "@radix-ui/react-id"; import { forwardRef, ReactNode } from "react"; -import { FormProvider, UseFormReturn } from "react-hook-form"; +import { FormProvider, useFormContext, UseFormReturn } from "react-hook-form"; import classNames from "@lib/classNames"; +import { useLocale } from "@lib/hooks/useLocale"; + +import { Alert } from "@components/ui/Alert"; type InputProps = Omit & { name: string }; export const Input = forwardRef(function Input(props, ref) { @@ -26,28 +29,68 @@ export function Label(props: JSX.IntrinsicElements["label"]) { ); } -export const TextField = forwardRef< - HTMLInputElement, - { - label: ReactNode; - } & React.ComponentProps & { - labelProps?: React.ComponentProps; - } ->(function TextField(props, ref) { - const id = useId(); - const { label, ...passThroughToInput } = props; +type InputFieldProps = { + label?: ReactNode; + addOnLeading?: ReactNode; +} & React.ComponentProps & { + labelProps?: React.ComponentProps; + }; - // TODO: use `useForm()` from RHF and get error state here too! +const InputField = forwardRef(function InputField(props, ref) { + const id = useId(); + const { t } = useLocale(); + const methods = useFormContext(); + const { + label = t(props.name), + labelProps, + placeholder = t(props.name + "_placeholder") !== props.name + "_placeholder" + ? t(props.name + "_placeholder") + : "", + className, + addOnLeading, + ...passThroughToInput + } = props; return (
-
); }); +export const TextField = forwardRef(function TextField(props, ref) { + return ; +}); + +export const PasswordField = forwardRef(function PasswordField( + props, + ref +) { + return ; +}); + +export const EmailField = forwardRef(function EmailField(props, ref) { + return ; +}); + // eslint-disable-next-line @typescript-eslint/no-explicit-any export const Form = forwardRef } & JSX.IntrinsicElements["form"]>( function Form(props, ref) { diff --git a/components/ui/UsernameInput.tsx b/components/ui/UsernameInput.tsx index cd3b615f0dd67..e1e8e83e7039a 100644 --- a/components/ui/UsernameInput.tsx +++ b/components/ui/UsernameInput.tsx @@ -4,6 +4,9 @@ interface UsernameInputProps extends React.ComponentPropsWithRef<"input"> { label?: string; } +/** + * @deprecated Use to achieve the same effect. + */ const UsernameInput = React.forwardRef((props, ref) => ( // todo, check if username is already taken here?
diff --git a/pages/api/auth/signup.ts b/pages/api/auth/signup.ts index b78fc1d28affc..7aeb2d95dc6ea 100644 --- a/pages/api/auth/signup.ts +++ b/pages/api/auth/signup.ts @@ -39,6 +39,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) email: userEmail, }, ], + AND: [ + { + emailVerified: { + not: null, + }, + }, + ], }, }); diff --git a/pages/auth/signup.tsx b/pages/auth/signup.tsx index ab40ead5c5a14..b1733fee93416 100644 --- a/pages/auth/signup.tsx +++ b/pages/auth/signup.tsx @@ -1,43 +1,50 @@ +import { GetServerSidePropsContext } from "next"; import { signIn } from "next-auth/client"; import { useRouter } from "next/router"; -import { useState } from "react"; +import { useForm, SubmitHandler, FormProvider } from "react-hook-form"; +import { asStringOrNull } from "@lib/asStringOrNull"; import { useLocale } from "@lib/hooks/useLocale"; import prisma from "@lib/prisma"; +import { inferSSRProps } from "@lib/types/inferSSRProps"; +import { EmailField, PasswordField, TextField } from "@components/form/fields"; import { HeadSeo } from "@components/seo/head-seo"; -import { UsernameInput } from "@components/ui/UsernameInput"; -import ErrorAlert from "@components/ui/alerts/Error"; +import { Alert } from "@components/ui/Alert"; +import Button from "@components/ui/Button"; -export default function Signup(props) { +type Props = inferSSRProps; + +type FormValues = { + username: string; + email: string; + password: string; + passwordcheck: string; + apiError: string; +}; + +export default function Signup({ email }: Props) { const { t } = useLocale(); const router = useRouter(); + const methods = useForm(); + const { + register, + formState: { errors, isSubmitting }, + } = methods; - const [hasErrors, setHasErrors] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); + methods.setValue("email", email); - const handleErrors = async (resp) => { + const handleErrors = async (resp: Response) => { if (!resp.ok) { const err = await resp.json(); throw new Error(err.message); } }; - const signUp = (e) => { - e.preventDefault(); - - if (e.target.password.value !== e.target.passwordcheck.value) { - throw new Error("Password mismatch"); - } - - const email: string = e.target.email.value; - const password: string = e.target.password.value; - - fetch("/api/auth/signup", { + const signUp: SubmitHandler = async (data) => { + await fetch("/api/auth/signup", { body: JSON.stringify({ - username: e.target.username.value, - password, - email, + ...data, }), headers: { "Content-Type": "application/json", @@ -45,104 +52,96 @@ export default function Signup(props) { method: "POST", }) .then(handleErrors) - .then(() => signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string })) + .then(async () => await signIn("Cal.com", { callbackUrl: (router.query.callbackUrl || "") as string })) .catch((err) => { - setHasErrors(true); - setErrorMessage(err.message); + methods.setError("apiError", { message: err.message }); }); }; return (
-

+

{t("create_your_account")}

-
-
- {hasErrors && } -
-
- -
-
- - -
-
- - + {/* TODO: Refactor as soon as /availability is live */} + + + {errors.apiError && } +
+ + {process.env.NEXT_PUBLIC_APP_URL}/ + + } + labelProps={{ className: "block text-sm font-medium text-gray-700" }} + className="flex-grow block w-full min-w-0 lowercase border-gray-300 rounded-none rounded-r-sm focus:ring-black focus:border-black sm:text-sm" + {...register("username")} required - placeholder="•••••••••••••" - className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-black focus:border-black sm:text-sm" /> -
-
- - + + + value === methods.watch("password") || (t("error_password_mismatch") as string), + })} + className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-black sm:text-sm" + /> +
+
+ +
-
- - + +
); } -export async function getServerSideProps(ctx) { - if (!ctx.query.token) { +export const getServerSideProps = async (ctx: GetServerSidePropsContext) => { + const token = asStringOrNull(ctx.query.token); + if (!token) { return { notFound: true, }; } const verificationRequest = await prisma.verificationRequest.findUnique({ where: { - token: ctx.query.token, + token, }, }); @@ -175,4 +174,4 @@ export async function getServerSideProps(ctx) { } return { props: { email: verificationRequest.identifier } }; -} +}; diff --git a/public/static/locales/en/common.json b/public/static/locales/en/common.json index 613b7e858d2a6..118eb8bf5cef4 100644 --- a/public/static/locales/en/common.json +++ b/public/static/locales/en/common.json @@ -251,6 +251,7 @@ "dark": "Dark", "automatically_adjust_theme": "Automatically adjust theme based on invitee preferences", "email": "Email", + "email_placeholder": "jdoe@example.com", "full_name": "Full name", "browse_api_documentation": "Browse our API documentation", "leverage_our_api": "Leverage our API for full control and customizability.", @@ -511,5 +512,7 @@ "delete_event_type": "Delete Event Type", "confirm_delete_event_type": "Yes, delete event type", "integrations": "Integrations", - "settings": "Settings" + "settings": "Settings", + "error_password_mismatch": "Passwords don't match.", + "error_required_field": "This field is required." } From d3879c61f5aad01d4ad3dc2e77febd4d9d410c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 10 Nov 2021 21:40:34 -0700 Subject: [PATCH 2/5] Type fixes for avilability form in onboarding --- components/form/fields.tsx | 47 ++++++++++++++++++++++++++------------ pages/getting-started.tsx | 2 +- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/components/form/fields.tsx b/components/form/fields.tsx index e6bbc8aeb8380..8c67a4aefd459 100644 --- a/components/form/fields.tsx +++ b/components/form/fields.tsx @@ -1,9 +1,11 @@ import { useId } from "@radix-ui/react-id"; -import { forwardRef, ReactNode } from "react"; -import { FormProvider, useFormContext, UseFormReturn } from "react-hook-form"; +import { forwardRef, ReactElement, ReactNode, Ref } from "react"; +import { FieldValues, FormProvider, SubmitHandler, useFormContext, UseFormReturn } from "react-hook-form"; import classNames from "@lib/classNames"; +import { getErrorFromUnknown } from "@lib/errors"; import { useLocale } from "@lib/hooks/useLocale"; +import showToast from "@lib/notification"; import { Alert } from "@components/ui/Alert"; @@ -91,20 +93,35 @@ export const EmailField = forwardRef(function return ; }); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const Form = forwardRef } & JSX.IntrinsicElements["form"]>( - function Form(props, ref) { - const { form, ...passThrough } = props; +type FormProps = { form: UseFormReturn; handleSubmit: SubmitHandler } & Omit< + JSX.IntrinsicElements["form"], + "onSubmit" +>; - return ( - -
- {props.children} -
-
- ); - } -); +const PlainForm = (props: FormProps, ref: Ref) => { + const { form, handleSubmit, ...passThrough } = props; + + return ( + +
{ + form + .handleSubmit(handleSubmit)(event) + .catch((err) => { + showToast(`${getErrorFromUnknown(err).message}`, "error"); + }); + }} + {...passThrough}> + {props.children} +
+
+ ); +}; + +export const Form = forwardRef(PlainForm) as ( + p: FormProps & { ref?: Ref } +) => ReactElement; export function FieldsetLegend(props: JSX.IntrinsicElements["legend"]) { return ( diff --git a/pages/getting-started.tsx b/pages/getting-started.tsx index ccc723470e762..22ec429bede6d 100644 --- a/pages/getting-started.tsx +++ b/pages/getting-started.tsx @@ -313,7 +313,7 @@ export default function Onboarding(props: inferSSRProps className="max-w-lg mx-auto text-black bg-white dark:bg-opacity-5 dark:text-white" form={availabilityForm} handleSubmit={async (values) => { From c9d7e67049621f8d51380508ecf2d6ba2ba1cacf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 10 Nov 2021 21:44:13 -0700 Subject: [PATCH 3/5] Re adds missing strings --- public/static/locales/en/common.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/public/static/locales/en/common.json b/public/static/locales/en/common.json index 118eb8bf5cef4..91f4e35d43e36 100644 --- a/public/static/locales/en/common.json +++ b/public/static/locales/en/common.json @@ -179,6 +179,7 @@ "connect": "Connect", "try_for_free": "Try it for free", "create_booking_link_with_calcom": "Create your own booking link with Cal.com", + "who": "Who", "what": "What", "when": "When", "where": "Where", @@ -217,10 +218,12 @@ "booking_already_cancelled": "This booking was already cancelled", "go_back_home": "Go back home", "or_go_back_home": "Or go back home", + "no_availability": "Unavailable", "no_meeting_found": "No Meeting Found", "no_meeting_found_description": "This meeting does not exist. Contact the meeting owner for an updated link.", "no_status_bookings_yet": "No {{status}} bookings, yet", "no_status_bookings_yet_description": "You have no {{status}} bookings. {{description}}", + "event_between_users": "{{eventName}} between {{host}} and {{attendeeName}}", "bookings": "Bookings", "bookings_description": "See upcoming and past events booked through your event type links.", "upcoming_bookings": "As soon as someone books a time with you it will show up here.", @@ -462,6 +465,7 @@ "billing": "Billing", "manage_your_billing_info": "Manage your billing information and cancel your subscription.", "availability": "Availability", + "availability_updated_successfully": "Availability updated successfully", "configure_availability": "Configure times when you are available for bookings.", "change_weekly_schedule": "Change your weekly schedule", "logo": "Logo", @@ -513,6 +517,18 @@ "confirm_delete_event_type": "Yes, delete event type", "integrations": "Integrations", "settings": "Settings", + "next_step": "Skip step", + "prev_step": "Prev step", + "installed": "Installed", + "disconnect": "Disconnect", + "embed_your_calendar": "Embed your calendar within your webpage", + "connect_your_favourite_apps": "Connect your favourite apps.", + "automation": "Automation", + "configure_how_your_event_types_interact": "Configure how your event types should interact with your calendars.", + "connect_an_additional_calendar": "Connect an additional calendar", + "conferencing": "Conferencing", + "calendar": "Calendar", + "not_installed": "Not installed", "error_password_mismatch": "Passwords don't match.", "error_required_field": "This field is required." } From 5498c7fd3540b405ed7a40341f9794a19fc1971b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 10 Nov 2021 22:07:36 -0700 Subject: [PATCH 4/5] Updates user availability in one query Tested and working correctly --- pages/api/schedule/index.ts | 40 +++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/pages/api/schedule/index.ts b/pages/api/schedule/index.ts index 00825a17cca6c..1cf9d2a951b9d 100644 --- a/pages/api/schedule/index.ts +++ b/pages/api/schedule/index.ts @@ -6,7 +6,7 @@ import prisma from "@lib/prisma"; import { TimeRange } from "@lib/types/schedule"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { - const session = await getSession({ req: req }); + const session = await getSession({ req }); const userId = session?.user?.id; if (!userId) { res.status(401).json({ message: "Not authenticated" }); @@ -17,7 +17,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) return res.status(400).json({ message: "Bad Request." }); } - const availability = req.body.schedule.reduce( + const availability: Availability[] = req.body.schedule.reduce( (availability: Availability[], times: TimeRange[], day: number) => { const addNewTime = (time: TimeRange) => ({ @@ -48,27 +48,29 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) if (req.method === "POST") { try { - await prisma.availability.deleteMany({ + await prisma.user.update({ where: { - userId, + id: userId, }, - }); - await Promise.all( - availability.map((schedule: Availability) => - prisma.availability.create({ - data: { - days: schedule.days, - startTime: schedule.startTime, - endTime: schedule.endTime, - user: { - connect: { - id: userId, - }, + data: { + availability: { + /* We delete user availabilty */ + deleteMany: { + userId: { + equals: userId, }, }, - }) - ) - ); + /* So we can replace it */ + createMany: { + data: availability.map((schedule) => ({ + days: schedule.days, + startTime: schedule.startTime, + endTime: schedule.endTime, + })), + }, + }, + }, + }); return res.status(200).json({ message: "created", }); From 00dda19fb5b538f955cb5e12e9b7730668cdb2be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Wed, 10 Nov 2021 22:33:50 -0700 Subject: [PATCH 5/5] Fixes seeder and tests --- components/ui/form/Schedule.tsx | 23 +++------------- lib/availability.ts | 47 +++++++++++++++++++++++++++++++++ pages/api/schedule/index.ts | 32 ++-------------------- pages/availability/index.tsx | 3 ++- pages/getting-started.tsx | 3 ++- scripts/seed.ts | 6 +++++ 6 files changed, 63 insertions(+), 51 deletions(-) create mode 100644 lib/availability.ts diff --git a/components/ui/form/Schedule.tsx b/components/ui/form/Schedule.tsx index b3076d44aff32..a07668254aecd 100644 --- a/components/ui/form/Schedule.tsx +++ b/components/ui/form/Schedule.tsx @@ -3,9 +3,10 @@ import dayjs, { Dayjs } from "dayjs"; import React, { useCallback, useState } from "react"; import { Controller, useFieldArray } from "react-hook-form"; +import { defaultDayRange } from "@lib/availability"; import { weekdayNames } from "@lib/core/i18n/weekday"; import { useLocale } from "@lib/hooks/useLocale"; -import { TimeRange, Schedule as ScheduleType } from "@lib/types/schedule"; +import { TimeRange } from "@lib/types/schedule"; import Button from "@components/ui/Button"; import Select from "@components/ui/form/Select"; @@ -30,22 +31,6 @@ const TIMES = (() => { })(); /** End Time Increments For Select */ -// sets the desired time in current date, needs to be current date for proper DST translation -const defaultDayRange: TimeRange = { - start: new Date(new Date().setHours(9, 0, 0, 0)), - end: new Date(new Date().setHours(17, 0, 0, 0)), -}; - -export const DEFAULT_SCHEDULE: ScheduleType = [ - [], - [defaultDayRange], - [defaultDayRange], - [defaultDayRange], - [defaultDayRange], - [defaultDayRange], - [], -]; - type Option = { readonly label: string; readonly value: number; @@ -139,7 +124,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { onChange={(e) => (e.target.checked ? replace([defaultDayRange]) : replace([]))} className="inline-block border-gray-300 rounded-sm focus:ring-neutral-500 text-neutral-900" /> - {weekday} + {weekday}
@@ -157,7 +142,7 @@ const ScheduleBlock = ({ name, day, weekday }: ScheduleBlockProps) => { />
))} - {!fields.length && t("no_availability")} + {!fields.length && t("no_availability")}