From ace27ca84e73a2d38a70025a6b0a9870dce6a83d Mon Sep 17 00:00:00 2001 From: Hariom Balhara Date: Thu, 22 Sep 2022 23:04:17 +0530 Subject: [PATCH] Use PremiumTextField in v2 (#4577) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use PremiumTextField in v2 * Fix tests * Fix Lint * Fix TS error * Fixes * Fix username input in self hosted scenario * Fix type error * Fix Tests * Fix username text field test Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Alex van Andel Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Omar López --- .../UsernameAvailability/PremiumTextfield.tsx | 63 +++++++++---------- .../UsernameTextfield.tsx | 24 +++---- .../pages/v2/settings/my-account/profile.tsx | 32 +++++++--- apps/web/playwright/change-username.e2e.ts | 4 +- .../stripepayment/api/subscription.ts | 2 - packages/trpc/server/createContext.ts | 1 + packages/trpc/server/routers/viewer.tsx | 1 + 7 files changed, 72 insertions(+), 55 deletions(-) diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx index 9f84cd7c9c..9c02a5555d 100644 --- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx @@ -5,14 +5,15 @@ import { MutableRefObject, useCallback, useEffect, useState } from "react"; import { getPremiumPlanMode, getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; import { fetchUsername } from "@calcom/lib/fetchUsername"; +import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { User } from "@calcom/prisma/client"; import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { inferQueryOutput, trpc } from "@calcom/trpc/react"; import type { AppRouter } from "@calcom/trpc/server/routers/_app"; -import Button from "@calcom/ui/Button"; import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog"; import { Icon, StarIconSolid } from "@calcom/ui/Icon"; +import { Button } from "@calcom/ui/v2"; import { Input, Label } from "@calcom/ui/v2"; export enum UsernameChangeStatusEnum { @@ -25,7 +26,7 @@ interface ICustomUsernameProps { currentUsername: string | undefined; setCurrentUsername: (value: string | undefined) => void; inputUsernameValue: string | undefined; - usernameRef: MutableRefObject; + usernameRef: MutableRefObject; setInputUsernameValue: (value: string) => void; onSuccessMutation?: () => void; onErrorMutation?: (error: TRPCClientErrorLike) => void; @@ -43,9 +44,8 @@ interface ICustomUsernameProps { | "plan" | "brandColor" | "darkBrandColor" - | "metadata" | "timeFormat" - | "allowDynamicBooking" + | "metadata" >; readonly?: boolean; } @@ -62,24 +62,13 @@ const obtainNewUsernameChangeCondition = ({ if (!userIsPremium && isNewUsernamePremium && !stripeCustomer?.paidForPremium) { return UsernameChangeStatusEnum.UPGRADE; } + if (userIsPremium && !isNewUsernamePremium && getPremiumPlanMode() === "subscription") { return UsernameChangeStatusEnum.DOWNGRADE; } return UsernameChangeStatusEnum.NORMAL; }; -const useIsUsernamePremium = (username: string) => { - const [isCurrentUsernamePremium, setIsCurrentUsernamePremium] = useState(false); - useEffect(() => { - (async () => { - if (!username) return; - const { data } = await fetchUsername(username); - setIsCurrentUsernamePremium(data.premium); - })(); - }, [username]); - return isCurrentUsernamePremium; -}; - const PremiumTextfield = (props: ICustomUsernameProps) => { const { t } = useLocale(); const { @@ -91,6 +80,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { onSuccessMutation, onErrorMutation, readonly: disabled, + user, } = props; const [usernameIsAvailable, setUsernameIsAvailable] = useState(false); const [markAsError, setMarkAsError] = useState(false); @@ -98,13 +88,14 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { const { paymentStatus: recentAttemptPaymentStatus } = router.query; const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false); const { data: stripeCustomer } = trpc.useQuery(["viewer.stripeCustomer"]); - const isCurrentUsernamePremium = useIsUsernamePremium(currentUsername || ""); + const isCurrentUsernamePremium = + user && user.metadata && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false; const [isInputUsernamePremium, setIsInputUsernamePremium] = useState(false); const debouncedApiCall = useCallback( debounce(async (username) => { const { data } = await fetchUsername(username); - setMarkAsError(!data.available && username !== currentUsername); + setMarkAsError(!data.available && username && username !== currentUsername); setIsInputUsernamePremium(data.premium); setUsernameIsAvailable(data.available); }, 150), @@ -183,7 +174,9 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { onClick={() => { if (currentUsername) { setInputUsernameValue(currentUsername); - usernameRef.current.value = currentUsername; + if (usernameRef.current) { + usernameRef.current.value = currentUsername; + } } }}> {t("cancel")} @@ -202,6 +195,20 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { } }; + let paymentMsg = !currentUsername ? ( + + You need to reserve your premium username for {getPremiumPlanPriceValue()} + + ) : null; + + if (recentAttemptPaymentStatus && recentAttemptPaymentStatus !== "paid") { + paymentMsg = ( + + Your payment could not be completed. Your username is still not reserved + + ); + } + return (
@@ -211,7 +218,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/ @@ -237,6 +244,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { value={inputUsernameValue} onChange={(event) => { event.preventDefault(); + // Reset payment status + delete router.query.paymentStatus; setInputUsernameValue(event.target.value); }} data-testid="username-input" @@ -248,7 +257,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => { isInputUsernamePremium ? "text-orange-400" : "", usernameIsAvailable ? "" : "" )}> - {isInputUsernamePremium ? : <>} + {isInputUsernamePremium ? : <>} {!isInputUsernamePremium && usernameIsAvailable ? : <>}
@@ -260,17 +269,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
)} - {paymentRequired ? ( - recentAttemptPaymentStatus && recentAttemptPaymentStatus !== "paid" ? ( - - Your payment could not be completed. Your username is still not reserved - - ) : ( - - You need to reserve your premium username for {getPremiumPlanPriceValue()} - - ) - ) : null} + {paymentMsg} {markAsError &&

Username is already taken

} {usernameIsAvailable && ( diff --git a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx index c891c05da7..af4fd1a4f6 100644 --- a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx @@ -7,16 +7,15 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { TRPCClientErrorLike } from "@calcom/trpc/client"; import { trpc } from "@calcom/trpc/react"; import { AppRouter } from "@calcom/trpc/server/routers/_app"; -import Button from "@calcom/ui/Button"; import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog"; import { Icon } from "@calcom/ui/Icon"; -import { Input, Label } from "@calcom/ui/v2"; +import { Input, Label, Button } from "@calcom/ui/v2"; interface ICustomUsernameProps { currentUsername: string | undefined; setCurrentUsername: (value: string | undefined) => void; inputUsernameValue: string | undefined; - usernameRef: MutableRefObject; + usernameRef: MutableRefObject; setInputUsernameValue: (value: string) => void; onSuccessMutation?: () => void; onErrorMutation?: (error: TRPCClientErrorLike) => void; @@ -73,15 +72,14 @@ const UsernameTextfield = (props: ICustomUsernameProps) => { }, }); - const ActionButtons = (props: { index: string }) => { - const { index } = props; + const ActionButtons = () => { return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
- +
{markAsError &&

Username is already taken

} {usernameIsAvailable && currentUsername !== inputUsernameValue && ( -
- +
+
)} diff --git a/apps/web/pages/v2/settings/my-account/profile.tsx b/apps/web/pages/v2/settings/my-account/profile.tsx index a1adce67e0..cc0bd94a45 100644 --- a/apps/web/pages/v2/settings/my-account/profile.tsx +++ b/apps/web/pages/v2/settings/my-account/profile.tsx @@ -22,6 +22,7 @@ import showToast from "@calcom/ui/v2/core/notifications"; import { SkeletonContainer, SkeletonText, SkeletonButton, SkeletonAvatar } from "@calcom/ui/v2/core/skeleton"; import TwoFactor from "@components/auth/TwoFactor"; +import { UsernameAvailability } from "@components/ui/UsernameAvailability"; const SkeletonLoader = () => { return ( @@ -48,6 +49,7 @@ interface DeleteAccountValues { const ProfileView = () => { const { t } = useLocale(); const utils = trpc.useContext(); + const usernameRef = useRef(null); const { data: user, isLoading } = trpc.useQuery(["viewer.me"]); const mutation = trpc.useMutation("viewer.updateProfile", { @@ -64,7 +66,11 @@ const ProfileView = () => { const [deleteAccountOpen, setDeleteAccountOpen] = useState(false); const [hasDeleteErrors, setHasDeleteErrors] = useState(false); const [deleteErrorMessage, setDeleteErrorMessage] = useState(""); - + const [currentUsername, setCurrentUsername] = useState(user?.username || undefined); + const [inputUsernameValue, setInputUsernameValue] = useState(currentUsername); + useEffect(() => { + if (user?.username) setCurrentUsername(user?.username); + }, [user?.username]); const form = useForm(); const emailMd5 = crypto @@ -156,8 +162,16 @@ const ProfileView = () => { [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`, [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), }; + const onSuccessfulUsernameUpdate = async () => { + showToast(t("settings_updated_successfully"), "success"); + await utils.invalidateQueries(["viewer.me"]); + }; - if (isLoading) return ; + const onErrorInUsernameUpdate = () => { + showToast(t("error_updating_settings"), "error"); + }; + + if (isLoading || !user) return ; return ( <> @@ -194,11 +208,15 @@ const ProfileView = () => { />
-
diff --git a/apps/web/playwright/change-username.e2e.ts b/apps/web/playwright/change-username.e2e.ts index e0ccf85c31..2dd39ceafb 100644 --- a/apps/web/playwright/change-username.e2e.ts +++ b/apps/web/playwright/change-username.e2e.ts @@ -35,10 +35,10 @@ test.describe("Change username on settings", () => { const usernameInput = page.locator("[data-testid=username-input]"); await usernameInput.fill("demousernamex"); - + await page.click("[data-testid=update-username-btn]"); await Promise.all([ + page.click("[data-testid=save-username]"), page.waitForResponse("**/viewer.updateProfile*"), - page.click('button[type="submit"]'), ]); const newUpdatedUser = await prisma.user.findUniqueOrThrow({ diff --git a/packages/app-store/stripepayment/api/subscription.ts b/packages/app-store/stripepayment/api/subscription.ts index 62af6520d8..a4c97fae60 100644 --- a/packages/app-store/stripepayment/api/subscription.ts +++ b/packages/app-store/stripepayment/api/subscription.ts @@ -25,7 +25,6 @@ export enum UsernameChangeStatusEnum { export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method === "GET") { const userId = req.session?.user.id; - let { intentUsername = null } = req.query; const { action, callbackUrl } = req.query; if (!userId || !intentUsername) { @@ -36,7 +35,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) intentUsername = intentUsername[0]; } const customerId = await getStripeCustomerIdFromUserId(userId); - if (!customerId) { res.status(404).json({ message: "Missing customer id" }); return; diff --git a/packages/trpc/server/createContext.ts b/packages/trpc/server/createContext.ts index 6295a85fb2..32fef242e3 100644 --- a/packages/trpc/server/createContext.ts +++ b/packages/trpc/server/createContext.ts @@ -73,6 +73,7 @@ async function getUserFromSession({ locale: true, timeFormat: true, trialEndsAt: true, + metadata: true, }, }); diff --git a/packages/trpc/server/routers/viewer.tsx b/packages/trpc/server/routers/viewer.tsx index 5d2782bc25..b966364490 100644 --- a/packages/trpc/server/routers/viewer.tsx +++ b/packages/trpc/server/routers/viewer.tsx @@ -179,6 +179,7 @@ const loggedInViewerRouter = createProtectedRouter() weekStart: user.weekStart, theme: user.theme, hideBranding: user.hideBranding, + metadata: user.metadata, }; }, })