import classNames from "classnames"; import { debounce, noop } from "lodash"; import { useRouter } from "next/router"; import type { RefCallback } from "react"; import { useEffect, useMemo, useState } from "react"; import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { fetchUsername } from "@calcom/lib/fetchUsername"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { TRPCClientErrorLike } from "@calcom/trpc/client"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import type { AppRouter } from "@calcom/trpc/server/routers/_app"; import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui"; import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon"; export enum UsernameChangeStatusEnum { UPGRADE = "UPGRADE", } interface ICustomUsernameProps { currentUsername: string | undefined; setCurrentUsername?: (newUsername: string) => void; inputUsernameValue: string | undefined; usernameRef: RefCallback; setInputUsernameValue: (value: string) => void; onSuccessMutation?: () => void; onErrorMutation?: (error: TRPCClientErrorLike) => void; readonly?: boolean; } const obtainNewUsernameChangeCondition = ({ userIsPremium, isNewUsernamePremium, }: { userIsPremium: boolean; isNewUsernamePremium: boolean; stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined; }) => { if (!userIsPremium && isNewUsernamePremium) { return UsernameChangeStatusEnum.UPGRADE; } }; const PremiumTextfield = (props: ICustomUsernameProps) => { const { t } = useLocale(); const { currentUsername, setCurrentUsername = noop, inputUsernameValue, setInputUsernameValue, usernameRef, onSuccessMutation, onErrorMutation, readonly: disabled, } = props; const [user] = trpc.viewer.me.useSuspenseQuery(); const [usernameIsAvailable, setUsernameIsAvailable] = useState(false); const [markAsError, setMarkAsError] = useState(false); const router = useRouter(); const { paymentStatus: recentAttemptPaymentStatus } = router.query; const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false); const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery(); const isCurrentUsernamePremium = user && user.metadata && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false; const [isInputUsernamePremium, setIsInputUsernamePremium] = useState(false); const debouncedApiCall = useMemo( () => debounce(async (username: string) => { const { data } = await fetchUsername(username); setMarkAsError(!data.available && !!currentUsername && username !== currentUsername); setIsInputUsernamePremium(data.premium); setUsernameIsAvailable(data.available); }, 150), [currentUsername] ); useEffect(() => { // Use the current username or if it's not set, use the one available from stripe setInputUsernameValue(currentUsername || stripeCustomer?.username || ""); }, [setInputUsernameValue, currentUsername, stripeCustomer?.username]); useEffect(() => { if (!inputUsernameValue) { debouncedApiCall.cancel(); return; } debouncedApiCall(inputUsernameValue); }, [debouncedApiCall, inputUsernameValue]); const utils = trpc.useContext(); const updateUsername = trpc.viewer.updateProfile.useMutation({ onSuccess: async () => { onSuccessMutation && (await onSuccessMutation()); setOpenDialogSaveUsername(false); }, onError: (error) => { onErrorMutation && onErrorMutation(error); }, async onSettled() { await utils.viewer.public.i18n.invalidate(); }, }); // when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for const paymentRequired = !currentUsername && stripeCustomer?.isPremium; const usernameChangeCondition = obtainNewUsernameChangeCondition({ userIsPremium: isCurrentUsernamePremium, isNewUsernamePremium: isInputUsernamePremium, stripeCustomer, }); const usernameFromStripe = stripeCustomer?.username; const paymentLink = `/api/integrations/stripepayment/subscription?intentUsername=${ inputUsernameValue || usernameFromStripe }&action=${usernameChangeCondition}&callbackUrl=${WEBAPP_URL}${router.asPath}`; const ActionButtons = () => { if (paymentRequired) { return (
); } if ((usernameIsAvailable || isInputUsernamePremium) && currentUsername !== inputUsernameValue) { return (
); } return <>; }; const saveUsername = () => { if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) { updateUsername.mutate({ username: inputUsernameValue, }); setCurrentUsername(inputUsernameValue); } }; 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 (
{ event.preventDefault(); // Reset payment status delete router.query.paymentStatus; setInputUsernameValue(event.target.value); }} data-testid="username-input" />
{isInputUsernamePremium ? : <>} {!isInputUsernamePremium && usernameIsAvailable ? ( ) : ( <> )}
{(usernameIsAvailable || isInputUsernamePremium) && currentUsername !== inputUsernameValue && (
)}
{paymentMsg} {markAsError &&

Username is already taken

} {usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (

{t("change_username_standard_to_premium")}

)} }>

{t("current_username")}

{currentUsername}

{t("new_username")}

{inputUsernameValue}

{/* redirect to checkout */} {usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && ( )} {/* Normal save */} {usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && ( )} setOpenDialogSaveUsername(false)}> {t("cancel")}
); }; export { PremiumTextfield };