+21









Nayan Bagale
GitHub
Tushar Bhatt
Udit Takkar
CarinaWolli
Peer Richelsen
Carina Wollendorfer
Benny Joo
Nizzy
nizzy
Syed Ali Shahbaz
GitHub Actions
Joe Au-Yeung
Hariom Balhara
Hariom Balhara
Eunjae Lee
Lauris Skraucis
Udit Takkar
Morgan
Vijay
supalarry
Alex van Andel
unknown
Bailey Pumfleet
Anik Dhabal Babu
Steven Tey
sean-brydon
amrit
Sanchit Tewari
Rajiv Sahal
Amit Sharma
7bff15cc47
* fix: replace react-hot-toast with sonner for toast notifications
* feat: add country code (#19264)
* perf: Move PlainChat up the hierarchy to remove rerenders (#19255)
* perf: Move PlainChat up the hierarchy to remove rerenders
* rmeove yarn lock
---------
Co-authored-by: Nizzy <nizabizaher@gmail.com>
Co-authored-by: nizzy <140507264+nizzyabi@users.noreply.github.com>
* hotfix (#19266)
* chore: release v4.9.11
* Handle cancelled status (#19272)
* feat: Use new /routing URL for routing forms (#19235)
* feat: Use new /routing URL for routing forms
* Fixes
* Fix bug
* precent crash
---------
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
* fix: remove section on /bookings if no data exists (#19261)
* feat: v2 slots new version (#18758)
* refactor: version old slots
* feat: init new slots endpoints
* chore: range format
* fix: duratin
* fix: duratin
* test: slot releaseAt
* refactor: reserve slot response
* refactor variable name
* docs: have new slots controller in docs
* feat: crud for slots reservations
* refactor: use exclude all for response
* docs
* chore: slots input service
* refactor mini
* refactor: remove unused imports
* docs
* handle orgSlug for dynamic events
* refactor: correct name
* docs
* add optional organizationSlug to BySlug search
* refactor: slot output format
* refactor: return seated slot info
* docs
* rename functions
* refactor: slots seated response
* fix: handle same username in org and non org
* refactor: test format
* fix: allow reservationDuration only for authed requests
* chore: check if auth user has permissions to define custom reservation duration
* refactor: split e2e test files
* refactor: invalid request exception codes
* chore: old slots controller uses all old versions
* refactor: remove depreceated fields (#19279)
* refactor: remove depreceated fields
* chore: use v2 endpoint
* chore: update V2 Api type
* chore
* chore: Refactor to remove `getServerSideProps` for /teams route (#19286)
* remove getServerSideProps
* refactor teams route
* chore: release v4.9.12
* fix: Salesforce - if no contact throw Error (#19271)
* Throw error if no contact is passed to createEvent
* chore: update error message
* chore
---------
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
* fix: missing await router controller (#19276)
* fix: Default people filter in bookings list for admin (#18035)
* default people filter for admin
* chore
* type check
* added test case
* made test concise to reduce flakiness
* add timeout to wait for second get response before counting
* update timeout
---------
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
* feat: add CSAT to V2 bookings return (#19278)
* init
* test: v2 bookings returns rating
---------
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
* chore: disable v2 booking seats safe parse error logs (#19292)
* revert: Reapply "fix: Multiple rr-hosts combine to create erroneous availabil… (#19263)
* Reapply "fix: Multiple rr-hosts combine to create erroneous availability (#18772)"
This reverts commit 5dbc6d9ca6.
* Only thing left is a diff to show which slots are removed
* Add logging around time slot results
* Setup logging through axiom
* .... forgot
* fix: type errors
* Only log when there are differences
* Updated from old which had a failing test to new logic
* chore: release v4.9.13
* fix: flaky test (#19294)
* fix: flaky test
* fix another test
* update
---------
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: unknown <adhabal2002@gmail.com>
* chore: calendars controller v2 docs (#19291)
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
* Merge branch 'main' into replace-toaster-with-sonner
* add ireland country code (#19117)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
* perf: separate client vs server code from insights layout (#19285)
* add UpgradeTipWrapper
* refactor insights/layout.tsx
---------
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
* feat: Use Folders to organize SMS links on Dub (#19248)
* feat: Use Folders to organize SMS links on Dub
* Update yarn.lock
---------
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
* fix: removing-all-members-toggle-for-fh (#19277)
* fix : removing all members toggle for fh
* ux enhancements
* adding placeholder
* chore: release v5.0.0
* fix flake (#19306)
* chore: migrate `/apps/routing-forms/[...pages]` to App Router (#18956)
* wip
* fix
* fix
* refactor
* rename to routingServerSidePropsConfig
* refactor
* refactor
* add routing-link to useIsBookingPage hook
* remove Head component
* fix
* redirect user to apps/routing-forms/forms if user goes to apps/routing-forms
* refactor
* remove log
* remove client component not needed
* clean up code
* remove unneeded metadata
* clean up further
* clean up further 2
* fix type check
* routing-link does not need shell
* Fix title for Routing Form public link and also remove any
* Remove ; in HTML
* Remove unnecessary page reload
---------
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
* chore: migrate api for team upgrade to App Router (#19100)
* remove old pages router api
* add new app router api
* fix
* fix redirect
* refactor
* fix: cut off and alignment bug (#19317)
* fix: assignment label cut-off
* fix: add all team members label is vertically aligned
* fix: nested scrolls on /bookings (#19284)
* fix: Can't change availability for managed event type (#19006)
* fix: flaky test organization.spec
* fix
* fix: bulk-update-in-scheduleId-in-detailed-view (#19295)
* fix: bulk-update-in-scheduleId-in-detailed-view
* minor-change
---------
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
* feat: prevent automatic query parameters when opening booking page (#18132)
- Resolve the user hindrance where the event's booking page should not have search parameters such as date and month in the initial page load
- Contains changes for the fix of issue #18094 as well as it is needed
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
* feat: atoms adjustments with ui v3 styles (#19320)
* refactor: Clean out old code, no longer needed (#19323)
* refactor: Clean out old code, no longer needed
* Removed a sneaky one keen to stay behind
* chore: release v5.0.1
* feat: Create `UserCreationService` and use in API V1 create user endpoint (#19150)
* Create `createUser` method in `userCreationService`
* Refactor `UserRepository` create method to accept prisma input and remove business logic
* API use `UserCreationService`
* Move slugify to service
* Use hashPassword instead
* Type fixes in `UserCreationService`
* Add `userCreationService` tests
* API accept data object
* Type fixes
* Add user _post test
* Add test for locked user
* Add locked param to log
* Add user repository tests
* Do not return locked status
* Explicitly pass `locked` prop
* Fix tests when locked isn't returned
* Fix tests
* Pass locked prop
* Edit test name
* Use logger
* Fix passing hashed password
* fix (#19298)
* fix: timezone input text is now visible in dark mode (#19325)
* feat: make first tab active by default (#19315)
* feat: make first tab active by default
* perf: optimize event types tabs rendering with useMemo
---------
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
* perf: Use one PageWrapper instance for all settings pages (#18752)
* add a layoutfile to use-page-wrapper route group
* refactor and move bookings status page to route group
* improve logic in pagesAndRewritePaths
* fix
* add use client directive
* move /403 and /500
* move /auth/error
* move /connect-and-join
* move /enterprise
* move /maintenance
* move /more
* move /upgrade
* chore: remove WithLayout from 8 low-risk single pages (#18745)
* move /403 and /500
* move /auth/error
* move /connect-and-join
* move /enterprise
* move /maintenance
* move /more
* move /upgrade
* move /availability pages
* move /event-types pages
* move /insights pages
* move /apps to use-page-wrapper group
* move /getting-started to use-page-wrapper group
* move /signup to use-page-wrapper group
* move /workflows to use-page-wrapper group
* move /video pages to use-page-wrapper group
* pass dehydratedState in layout
* pass dehydratedState in layout
* move settings/(settings-layout) to use-page-wrapper
* move settings/(admin-layout) to use-page-wrapper
* move remaining settings pages to (use-page-wrapper)
* fix test
* remove unused code
* remove unused files
* fix type check
* remove test tag
* remove page
* RSC should not default export client components
* fix: Can't use unsaved booking question as variable in event name (#18604)
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
* chore: remove WithLayout from not-found.tsx (#19331)
* fix: redirect logic in /settings/admin/apps (#19332)
* chore: v3 fixed filters, new profile view (#19312)
* fixed filters with badge
* aligned recurrence in bookings
* removed new badge
* new profile view
* fixed try for free button
* fixed form input
* fix: Issues with date changes in column view (#19258)
* fix: coulmn view date change
* Update store.ts
---------
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
* chore: fixed tooltip position for routing form (#19267)
* fixed tooltip position for routing form
* fixed offset
* fix: Fairness when routing (#18550)
* Partial push of new filters
* Let's always return the passed host array when a filter doesn't need to be applied
* fix: isFixed typing in same round robin host check
* Add main bulk of rewritten logic for routed fairness support
* Remove console.log
* Add getAttributeWithEnabledWeights to AttributeRepository
* Add OOO entry repository
* Use getOrderedListOfLuckyUsers in lead threshold filtering
* Shortfall already includes weight
* test against minShortfall
* first attempt to fix fallbacks
* Added testcase updates
* Partially fix filterHosts test
* Some test updates
* Removed email field from normalization
* fix no available users found error
* fix unit test
* remove fallback from same host reschedule return
* fixes and improvements for findQualifiedHosts
* include fallback logic in handleNewBooking
* use fallback in handleNewBooking
* fix for contact owner in handleNewBooking
* WIP - comment
* pass routingFormResponseId
* fix type error
* pass routingFormResponse
* fixes in treshold logic
* remove reason
* merge qualified and fallback availabilities
* fix type error
* add and fix tests
* clean up test file
* fix filterHostsBySameRoundRobinHost tests
* fix findQualifiedHosts.test.ts
* fix getRoutedUsers.test.ts
* fix tests
* fix isRerouting check
* fix start time for two weeks check
* fix fallback hosts
* fix test
* fallback hosts should also include qualified hosts
* fix type error
* fix typos
* add contact owner to fallbacks
* fix typo
* fix qualifiedRRUsers
* set fixed users when no hosts
* add tets + code clean up
* add test for reschedule with same host
* fix typo
* add more tests for findQualifiedHosts
* remove skip from test
* add comment
* remove unused code
* fix fallback users in loadAndValidateUsers
* improve naming for fallback
* rename missed fallback variable
* fix reschedule with same rr host in handleNewBooking
* fixes for fallback slots
* add more tests + small fixes for contact owner
* fix tests and type errors
* add logs for fallback
* add type:json to logger
* Fix small merge issue
* fix end time if two weeks are cut off
* add more tests for contact owner availability check
* add new logic for two weeks fallback
* fix reschedule same RR host test
---------
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
* fix: EditLocationDialog footer positioning and structure (#19335)
* fix: EditLocationDialog footer positioning and structure
* fix: make RescheduleDialog footer layout and button structure right
* fix: input dialogue glitch now works great
* made tests happy
---------
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
* Merge branch 'main' into replace-toaster-with-sonner
* chore: remove DefaultToast from toast.stories.mdx
---------
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Nizzy <nizabizaher@gmail.com>
Co-authored-by: nizzy <140507264+nizzyabi@users.noreply.github.com>
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: GitHub Actions <github-actions@github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Lauris Skraucis <lauris.skraucis@gmail.com>
Co-authored-by: Udit Takkar <udit222001@gmail.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
Co-authored-by: Vijay <vijayraghav22@gmail.com>
Co-authored-by: supalarry <laurisskraucis@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Steven Tey <stevensteel97@gmail.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: amrit <iamamrit27@gmail.com>
Co-authored-by: Sanchit Tewari <46245135+sanchitttt@users.noreply.github.com>
Co-authored-by: Rajiv Sahal <sahalrajiv-extc@atharvacoe.ac.in>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
707 lines
28 KiB
TypeScript
707 lines
28 KiB
TypeScript
"use client";
|
|
|
|
import { Analytics as DubAnalytics } from "@dub/analytics/react";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { signIn } from "next-auth/react";
|
|
import { Trans } from "next-i18next";
|
|
import dynamic from "next/dynamic";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import Script from "next/script";
|
|
import { useState, useEffect } from "react";
|
|
import type { SubmitHandler } from "react-hook-form";
|
|
import { useForm, useFormContext } from "react-hook-form";
|
|
import { Toaster } from "sonner";
|
|
import { z } from "zod";
|
|
|
|
import getStripe from "@calcom/app-store/stripepayment/lib/client";
|
|
import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
|
|
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
|
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import { classNames } from "@calcom/lib";
|
|
import {
|
|
APP_NAME,
|
|
URL_PROTOCOL_REGEX,
|
|
IS_CALCOM,
|
|
IS_EUROPE,
|
|
WEBAPP_URL,
|
|
CLOUDFLARE_SITE_ID,
|
|
WEBSITE_PRIVACY_POLICY_URL,
|
|
WEBSITE_TERMS_URL,
|
|
WEBSITE_URL,
|
|
} from "@calcom/lib/constants";
|
|
import { isENVDev } from "@calcom/lib/env";
|
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
|
import { pushGTMEvent } from "@calcom/lib/gtm";
|
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
|
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
|
import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
|
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
|
import { Button, PasswordField, TextField, Form, Alert, CheckboxField, Icon, showToast } from "@calcom/ui";
|
|
|
|
import type { getServerSideProps } from "@lib/signup/getServerSideProps";
|
|
|
|
const signupSchema = apiSignupSchema.extend({
|
|
apiError: z.string().optional(), // Needed to display API errors doesn't get passed to the API
|
|
cfToken: z.string().optional(),
|
|
});
|
|
|
|
const TurnstileCaptcha = dynamic(() => import("@calcom/features/auth/Turnstile"), { ssr: false });
|
|
|
|
type FormValues = z.infer<typeof signupSchema>;
|
|
|
|
export type SignupProps = inferSSRProps<typeof getServerSideProps>;
|
|
|
|
const FEATURES = [
|
|
{
|
|
title: "connect_all_calendars",
|
|
description: "connect_all_calendars_description",
|
|
i18nOptions: {
|
|
appName: APP_NAME,
|
|
},
|
|
icon: "calendar-heart" as const,
|
|
},
|
|
{
|
|
title: "set_availability",
|
|
description: "set_availbility_description",
|
|
icon: "users" as const,
|
|
},
|
|
{
|
|
title: "share_a_link_or_embed",
|
|
description: "share_a_link_or_embed_description",
|
|
icon: "link-2" as const,
|
|
i18nOptions: {
|
|
appName: APP_NAME,
|
|
},
|
|
},
|
|
];
|
|
|
|
function UsernameField({
|
|
username,
|
|
setPremium,
|
|
premium,
|
|
setUsernameTaken,
|
|
orgSlug,
|
|
usernameTaken,
|
|
disabled,
|
|
...props
|
|
}: React.ComponentProps<typeof TextField> & {
|
|
username: string;
|
|
setPremium: (value: boolean) => void;
|
|
premium: boolean;
|
|
usernameTaken: boolean;
|
|
orgSlug?: string;
|
|
setUsernameTaken: (value: boolean) => void;
|
|
}) {
|
|
const { t } = useLocale();
|
|
const { register, formState } = useFormContext<FormValues>();
|
|
const debouncedUsername = useDebounce(username, 600);
|
|
|
|
useEffect(() => {
|
|
if (formState.isSubmitting || formState.isSubmitSuccessful) return;
|
|
|
|
async function checkUsername() {
|
|
// If the username can't be changed, there is no point in doing the username availability check
|
|
if (disabled) return;
|
|
if (!debouncedUsername) {
|
|
setPremium(false);
|
|
setUsernameTaken(false);
|
|
return;
|
|
}
|
|
fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
|
|
setPremium(data.premium);
|
|
setUsernameTaken(!data.available);
|
|
});
|
|
}
|
|
checkUsername();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]);
|
|
|
|
return (
|
|
<div>
|
|
<TextField
|
|
disabled={disabled}
|
|
{...props}
|
|
{...register("username")}
|
|
data-testid="signup-usernamefield"
|
|
/>
|
|
{(!formState.isSubmitting || !formState.isSubmitted) && (
|
|
<div className="text-gray text-default flex items-center text-sm">
|
|
<div className="text-sm ">
|
|
{usernameTaken ? (
|
|
<div className="text-error flex items-center">
|
|
<Icon name="info" className="mr-1 inline-block h-4 w-4" />
|
|
<p>{t("already_in_use_error")}</p>
|
|
</div>
|
|
) : premium ? (
|
|
<div data-testid="premium-username-warning" className="flex items-center">
|
|
<Icon name="star" className="mr-1 inline-block h-4 w-4" />
|
|
<p>
|
|
{t("premium_username", {
|
|
price: getPremiumPlanPriceValue(),
|
|
})}
|
|
</p>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function addOrUpdateQueryParam(url: string, key: string, value: string) {
|
|
const separator = url.includes("?") ? "&" : "?";
|
|
const param = `${key}=${encodeURIComponent(value)}`;
|
|
return `${url}${separator}${param}`;
|
|
}
|
|
|
|
export default function Signup({
|
|
prepopulateFormValues,
|
|
token,
|
|
orgSlug,
|
|
isGoogleLoginEnabled,
|
|
isSAMLLoginEnabled,
|
|
orgAutoAcceptEmail,
|
|
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();
|
|
const router = useRouter();
|
|
const formMethods = useForm<FormValues>({
|
|
resolver: zodResolver(signupSchema),
|
|
defaultValues: prepopulateFormValues satisfies FormValues,
|
|
mode: "onChange",
|
|
});
|
|
const {
|
|
register,
|
|
watch,
|
|
formState: { isSubmitting, errors, isSubmitSuccessful },
|
|
} = formMethods;
|
|
|
|
useEffect(() => {
|
|
if (redirectUrl) {
|
|
localStorage.setItem("onBoardingRedirect", redirectUrl);
|
|
}
|
|
}, [redirectUrl]);
|
|
|
|
const [userConsentToCookie, setUserConsentToCookie] = useState(false); // No need to be checked for user to proceed
|
|
|
|
function handleConsentChange(consent: boolean) {
|
|
setUserConsentToCookie(!consent);
|
|
}
|
|
|
|
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
|
|
const displayBackButton = token ? false : displayEmailForm;
|
|
|
|
const handleErrorsAndStripe = async (resp: Response) => {
|
|
if (!resp.ok) {
|
|
const err = await resp.json();
|
|
if (err.checkoutSessionId) {
|
|
const stripe = await getStripe();
|
|
if (stripe) {
|
|
console.log("Redirecting to stripe checkout");
|
|
const { error } = await stripe.redirectToCheckout({
|
|
sessionId: err.checkoutSessionId,
|
|
});
|
|
console.warn(error.message);
|
|
}
|
|
} else {
|
|
throw new Error(err.message);
|
|
}
|
|
}
|
|
};
|
|
|
|
const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new");
|
|
|
|
const signUp: SubmitHandler<FormValues> = async (_data) => {
|
|
const { cfToken, ...data } = _data;
|
|
await fetch("/api/auth/signup", {
|
|
body: JSON.stringify({
|
|
...data,
|
|
language: i18n.language,
|
|
token,
|
|
}),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
"cf-access-token": cfToken ?? "invalid-token",
|
|
},
|
|
method: "POST",
|
|
})
|
|
.then(handleErrorsAndStripe)
|
|
.then(async () => {
|
|
if (process.env.NEXT_PUBLIC_GTM_ID)
|
|
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
|
|
|
|
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
|
|
|
|
const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : "getting-started";
|
|
const gettingStartedWithPlatform = "settings/platform/new";
|
|
|
|
const constructCallBackIfUrlPresent = () => {
|
|
if (isOrgInviteByLink) {
|
|
return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`;
|
|
}
|
|
|
|
return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup");
|
|
};
|
|
|
|
const constructCallBackIfUrlNotPresent = () => {
|
|
if (!!isPlatformUser) {
|
|
return `${WEBAPP_URL}/${gettingStartedWithPlatform}?from=signup`;
|
|
}
|
|
|
|
return `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`;
|
|
};
|
|
|
|
const constructCallBackUrl = () => {
|
|
const callbackUrlSearchParams = searchParams?.get("callbackUrl");
|
|
|
|
return !!callbackUrlSearchParams
|
|
? constructCallBackIfUrlPresent()
|
|
: constructCallBackIfUrlNotPresent();
|
|
};
|
|
|
|
const callBackUrl = constructCallBackUrl();
|
|
|
|
await signIn<"credentials">("credentials", {
|
|
...data,
|
|
callbackUrl: callBackUrl,
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
formMethods.setError("apiError", { message: err.message });
|
|
});
|
|
};
|
|
|
|
return (
|
|
<>
|
|
{IS_CALCOM && (!IS_EUROPE || userConsentToCookie) ? (
|
|
<>
|
|
{process.env.NEXT_PUBLIC_GTM_ID && (
|
|
<>
|
|
<Script
|
|
id="gtm-init-script"
|
|
// It is strictly not necessary to disable, but in a future update of react/no-danger this will error.
|
|
// And we don't want it to error here anyways
|
|
// eslint-disable-next-line react/no-danger
|
|
dangerouslySetInnerHTML={{
|
|
__html: `(function (w, d, s, l, i) {
|
|
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
|
var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : '';
|
|
j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
|
})(window, document, 'script', 'dataLayer', '${process.env.NEXT_PUBLIC_GTM_ID}');`,
|
|
}}
|
|
/>
|
|
<noscript
|
|
// eslint-disable-next-line react/no-danger
|
|
dangerouslySetInnerHTML={{
|
|
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
|
|
}}
|
|
/>
|
|
</>
|
|
)}
|
|
<DubAnalytics
|
|
cookieOptions={{
|
|
domain: isENVDev ? undefined : `.${new URL(WEBSITE_URL).hostname}`,
|
|
}}
|
|
/>
|
|
</>
|
|
) : null}
|
|
<div
|
|
className={classNames(
|
|
"light bg-muted 2xl:bg-default flex min-h-screen w-full flex-col items-center justify-center [--cal-brand:#111827] dark:[--cal-brand:#FFFFFF]",
|
|
"[--cal-brand-subtle:#9CA3AF]",
|
|
"[--cal-brand-text:#FFFFFF] dark:[--cal-brand-text:#000000]",
|
|
"[--cal-brand-emphasis:#101010] dark:[--cal-brand-emphasis:#e1e1e1] "
|
|
)}>
|
|
<div className="bg-muted 2xl:border-subtle grid w-full max-w-[1440px] grid-cols-1 grid-rows-1 overflow-hidden lg:grid-cols-2 2xl:rounded-[20px] 2xl:border 2xl:py-6">
|
|
{/* 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-24 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"
|
|
data-testid="signup-back-button"
|
|
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")}
|
|
</h1>
|
|
{IS_CALCOM ? (
|
|
<p className="text-subtle text-base font-medium leading-5">{t("cal_signup_description")}</p>
|
|
) : (
|
|
<p className="text-subtle text-base font-medium leading-5">
|
|
{t("calcom_explained", {
|
|
appName: APP_NAME,
|
|
})}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Form Container */}
|
|
{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"
|
|
/>
|
|
|
|
{/* 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
|
|
data-testid="signup-cookie-content-checkbox"
|
|
onChange={() => handleConsentChange(userConsentToCookie)}
|
|
description={t("cookie_consent_checkbox")}
|
|
/>
|
|
{errors.apiError && (
|
|
<Alert
|
|
className="mb-3"
|
|
severity="error"
|
|
message={errors.apiError?.message}
|
|
data-testid="signup-error-message"
|
|
/>
|
|
)}
|
|
{isSamlSignup ? (
|
|
<Button
|
|
data-testid="saml-submit-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) {
|
|
// should not be reached but needed to bypass type errors
|
|
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-12">
|
|
{/* 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"
|
|
/>
|
|
}
|
|
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);
|
|
}}>
|
|
{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={isGoogleLoading}
|
|
className={classNames("w-full justify-center rounded-md text-center")}
|
|
onClick={() => {
|
|
setDisplayEmailForm(true);
|
|
setIsSamlSignup(false);
|
|
}}
|
|
data-testid="continue-with-email-button">
|
|
{t("continue_with_email")}
|
|
</Button>
|
|
{isSAMLLoginEnabled && (
|
|
<Button
|
|
data-testid="continue-with-saml-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>
|
|
)}
|
|
|
|
{/* 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">
|
|
<div className="flex gap-1">
|
|
<p className="text-subtle">{t("already_have_account")}</p>
|
|
<Link href="/auth/login" className="text-emphasis hover:underline">
|
|
{t("sign_in")}
|
|
</Link>
|
|
</div>
|
|
<div className="text-subtle">
|
|
<Trans
|
|
i18nKey="signing_up_terms"
|
|
components={[
|
|
<Link
|
|
className="text-emphasis hover:underline"
|
|
key="terms"
|
|
href={`${WEBSITE_TERMS_URL}`}
|
|
target="_blank">
|
|
Terms
|
|
</Link>,
|
|
<Link
|
|
className="text-emphasis hover:underline"
|
|
key="privacy"
|
|
href={`${WEBSITE_PRIVACY_POLICY_URL}`}
|
|
target="_blank">
|
|
Privacy Policy.
|
|
</Link>,
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
|
|
{IS_CALCOM && (
|
|
<>
|
|
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
|
<div>
|
|
<img
|
|
src="/product-cards/product-of-the-day.svg"
|
|
className="h-[34px] w-full dark:invert"
|
|
alt="Cal.com was Product of the Day at ProductHunt"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<img
|
|
src="/product-cards/product-of-the-week.svg"
|
|
className="h-[34px] w-full dark:invert"
|
|
alt="Cal.com was Product of the Week at ProductHunt"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<img
|
|
src="/product-cards/product-of-the-month.svg"
|
|
className="h-[34px] w-full dark:invert"
|
|
alt="Cal.com was Product of the Month at ProductHunt"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
|
<div>
|
|
<img
|
|
src="/product-cards/producthunt.svg"
|
|
className="h-[54px] w-full"
|
|
alt="ProductHunt Rating of 5 Stars"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<img
|
|
src="/product-cards/google-reviews.svg"
|
|
className="h-[54px] w-full"
|
|
alt="Google Reviews Rating of 4.7 Stars"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<img
|
|
src="/product-cards/g2.svg"
|
|
className="h-[54px] w-full"
|
|
alt="G2 Rating of 4.7 Stars"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
|
|
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
|
|
<img
|
|
className="hidden dark:block"
|
|
src="/mock-event-type-list-dark.svg"
|
|
alt="Cal.com Booking Page"
|
|
/>
|
|
</div>
|
|
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
|
|
{FEATURES.map((feature) => (
|
|
<>
|
|
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
|
|
<div className="text-emphasis items-center">
|
|
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
|
|
<span className="text-sm font-medium">{t(feature.title)}</span>
|
|
</div>
|
|
<div className="text-subtle text-sm">
|
|
<p>
|
|
{t(
|
|
feature.description,
|
|
feature.i18nOptions && {
|
|
...feature.i18nOptions,
|
|
}
|
|
)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<Toaster position="bottom-right" />
|
|
</div>
|
|
</>
|
|
);
|
|
}
|