feat: add customReplyToEmail for EventTypeSettings atom (#23686)
* init: endpoint for fetching verified emails * fix: enable custom reply to email in frontend * init endpoint to add verified emails * add verified emails option for platform in frontend * fixup: move useGetVerifiedEmails hook to correct folder * update atoms module * fixup: teamId should be string * add methond to fetch team member emails * update logic to fetch and add emails * fixup: append client id with email * fixup: pass teamId for fetching verified emails * fixup: simplify check for existing emails * fix: cleanup comments * fix: implement code rabbit feedback * fix: add translations * fixup: update transaltions * fix: update logic for addVerifiedEmail * add changesets * fix: implement PR feedback
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@calcom/atoms": minor
|
||||
---
|
||||
|
||||
This PR adds customReplyEmailTo feature for EventTypeSettings atom
|
||||
@@ -17,6 +17,7 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/teams/inde
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { TeamsEventTypesModule } from "@/modules/teams/event-types/teams-event-types.module";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
@@ -36,6 +37,7 @@ import { Module } from "@nestjs/common";
|
||||
SchedulesAtomsService,
|
||||
VerificationAtomsService,
|
||||
RedisService,
|
||||
TeamsRepository,
|
||||
],
|
||||
exports: [EventTypesAtomService],
|
||||
controllers: [
|
||||
|
||||
@@ -64,4 +64,47 @@ export class AtomsRepository {
|
||||
|
||||
return userTeams;
|
||||
}
|
||||
|
||||
async getSecondaryEmails(userId: number) {
|
||||
return await this.dbRead.prisma.secondaryEmail.findMany({
|
||||
where: {
|
||||
userId,
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getExistingSecondaryEmailByUserAndEmail(userId: number, email: string) {
|
||||
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||
where: {
|
||||
userId_email: { userId, email },
|
||||
},
|
||||
});
|
||||
|
||||
return existingSecondaryEmailRecord?.email;
|
||||
}
|
||||
|
||||
async getExistingSecondaryEmail(email: string) {
|
||||
const existingSecondaryEmailRecord = await this.dbRead.prisma.secondaryEmail.findUnique({
|
||||
where: {
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
return existingSecondaryEmailRecord?.email;
|
||||
}
|
||||
|
||||
async addSecondaryEmail(userId: number, email: string) {
|
||||
const existingSecondaryEmailRecord = await this.dbWrite.prisma.secondaryEmail.create({
|
||||
data: {
|
||||
userId,
|
||||
email,
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
return existingSecondaryEmailRecord?.email;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { Throttle } from "@/lib/endpoint-throttler-decorator";
|
||||
import { AddVerifiedEmailInput } from "@/modules/atoms/inputs/add-verified-email.input";
|
||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||
import { GetVerifiedEmailsParams } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||
import { GetVerifiedEmailsOutput } from "@/modules/atoms/outputs/get-verified-emails-output";
|
||||
import { SendVerificationEmailOutput } from "@/modules/atoms/outputs/send-verification-email.output";
|
||||
import { VerifyEmailCodeOutput } from "@/modules/atoms/outputs/verify-email-code.output";
|
||||
import { VerificationAtomsService } from "@/modules/atoms/services/verification-atom.service";
|
||||
@@ -105,4 +108,44 @@ export class AtomsVerificationController {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/emails/verified-emails")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async getVerifiedEmails(
|
||||
@Query() query: GetVerifiedEmailsParams,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetVerifiedEmailsOutput> {
|
||||
const verifiedEmails = await this.verificationService.getVerifiedEmails({
|
||||
userId: user.id,
|
||||
userEmail: user.email,
|
||||
teamId: query.teamId ? Number(query.teamId) : undefined,
|
||||
});
|
||||
|
||||
return {
|
||||
data: verifiedEmails,
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/emails/verified-emails")
|
||||
@Version(VERSION_NEUTRAL)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async addVerifiedEmails(
|
||||
@Body() body: AddVerifiedEmailInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ApiResponse<{ emailVerified: boolean }>> {
|
||||
const emailVerified = await this.verificationService.addVerifiedEmail({
|
||||
userId: user.id,
|
||||
existingPrimaryEmail: user.email,
|
||||
email: body.email,
|
||||
});
|
||||
|
||||
return {
|
||||
data: { emailVerified },
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { IsEmail } from "class-validator";
|
||||
|
||||
export class AddVerifiedEmailInput {
|
||||
@ApiProperty({ example: "user@example.com" })
|
||||
@IsEmail()
|
||||
email!: string;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNumber, IsOptional, IsString, IsEmail } from "class-validator";
|
||||
|
||||
export class GetVerifiedEmailsParams {
|
||||
@ApiPropertyOptional({ example: "12345" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
teamId?: string;
|
||||
}
|
||||
|
||||
export class GetVerifiedEmailsInput {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
userId!: number;
|
||||
|
||||
@ApiProperty()
|
||||
@IsEmail()
|
||||
@IsString()
|
||||
userEmail!: string;
|
||||
|
||||
@ApiPropertyOptional()
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
teamId?: number;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsString, IsArray } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class GetVerifiedEmailsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsString()
|
||||
@Expose()
|
||||
readonly status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
readonly data!: string[];
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { CheckEmailVerificationRequiredParams } from "@/modules/atoms/inputs/check-email-verification-required-params";
|
||||
import { GetVerifiedEmailsInput } from "@/modules/atoms/inputs/get-verified-emails-params";
|
||||
import { SendVerificationEmailInput } from "@/modules/atoms/inputs/send-verification-email.input";
|
||||
import { VerifyEmailCodeInput } from "@/modules/atoms/inputs/verify-email-code.input";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
|
||||
@@ -13,6 +16,11 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class VerificationAtomsService {
|
||||
constructor(
|
||||
private readonly atomsRepository: AtomsRepository,
|
||||
private readonly teamsRepository: TeamsRepository
|
||||
) {}
|
||||
|
||||
async checkEmailVerificationRequired(input: CheckEmailVerificationRequiredParams) {
|
||||
return await checkEmailVerificationRequired(input);
|
||||
}
|
||||
@@ -61,4 +69,78 @@ export class VerificationAtomsService {
|
||||
isVerifyingEmail: input.isVerifyingEmail,
|
||||
});
|
||||
}
|
||||
|
||||
async getVerifiedEmails(input: GetVerifiedEmailsInput): Promise<string[]> {
|
||||
const { userId, userEmail, teamId } = input;
|
||||
const userEmailWithoutOauthClientId = this.removeClientIdFromEmail(userEmail);
|
||||
|
||||
if (teamId) {
|
||||
const verifiedEmails: string[] = [];
|
||||
const teamMembers = await this.teamsRepository.getTeamMemberEmails(teamId);
|
||||
|
||||
if (teamMembers.length === 0) {
|
||||
return verifiedEmails;
|
||||
}
|
||||
|
||||
teamMembers.forEach((member) => {
|
||||
const memberEmailWithoutOauthClientId = this.removeClientIdFromEmail(member.email);
|
||||
|
||||
verifiedEmails.push(memberEmailWithoutOauthClientId);
|
||||
member.secondaryEmails.forEach((secondaryEmail) => {
|
||||
verifiedEmails.push(this.removeClientIdFromEmail(secondaryEmail.email));
|
||||
});
|
||||
});
|
||||
|
||||
return verifiedEmails;
|
||||
}
|
||||
|
||||
let verifiedEmails = [userEmailWithoutOauthClientId];
|
||||
|
||||
const secondaryEmails = await this.atomsRepository.getSecondaryEmails(userId);
|
||||
verifiedEmails = verifiedEmails.concat(
|
||||
secondaryEmails.map((secondaryEmail) => this.removeClientIdFromEmail(secondaryEmail.email))
|
||||
);
|
||||
|
||||
return verifiedEmails;
|
||||
}
|
||||
|
||||
async addVerifiedEmail({
|
||||
userId,
|
||||
existingPrimaryEmail,
|
||||
email,
|
||||
}: {
|
||||
userId: number;
|
||||
existingPrimaryEmail: string;
|
||||
email: string;
|
||||
}): Promise<boolean> {
|
||||
const existingSecondaryEmail = await this.atomsRepository.getExistingSecondaryEmailByUserAndEmail(
|
||||
userId,
|
||||
email
|
||||
);
|
||||
const alreadyExistingEmail = await this.atomsRepository.getExistingSecondaryEmail(email);
|
||||
|
||||
if (alreadyExistingEmail) {
|
||||
throw new BadRequestException("Email already exists");
|
||||
}
|
||||
|
||||
if (existingPrimaryEmail === email || existingSecondaryEmail === email) {
|
||||
return true;
|
||||
}
|
||||
|
||||
await this.atomsRepository.addSecondaryEmail(userId, email);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
removeClientIdFromEmail(email: string): string {
|
||||
const [localPart, domain] = email.split("@");
|
||||
const localPartSegments = localPart.split("+");
|
||||
|
||||
localPartSegments.pop();
|
||||
|
||||
const baseEmail = localPartSegments.join("+");
|
||||
const normalizedEmail = `${baseEmail}@${domain}`;
|
||||
|
||||
return normalizedEmail;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,4 +120,30 @@ export class TeamsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamMemberEmails(teamId: number) {
|
||||
return this.dbRead.prisma.user.findMany({
|
||||
where: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
secondaryEmails: {
|
||||
where: {
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,7 @@
|
||||
"org_upgraded_successfully": "Your Organization was upgraded successfully!",
|
||||
"use_link_to_reset_password": "Use the link below to reset your password",
|
||||
"hey_there": "Hey there",
|
||||
"there": "there",
|
||||
"forgot_your_password_calcom": "Forgot your password? - {{appName}}",
|
||||
"delete_webhook_confirmation_message": "Are you sure you want to delete this webhook? You will no longer receive {{appName}} meeting data at a specified URL, in real-time, when an event is scheduled or canceled.",
|
||||
"confirm_delete_webhook": "Yes, delete webhook",
|
||||
@@ -3344,6 +3345,8 @@
|
||||
"hide_organizer_email_description": "Hide organizer's email address from the booking screen, email notifications, and calendar events",
|
||||
"you_and_conjunction": "You &",
|
||||
"select_verified_email": "Select a verified email",
|
||||
"add_verified_email": "Add verified email",
|
||||
"add_verified_emails": "Add verified emails",
|
||||
"custom_reply_to_email_title": "Custom 'Reply-To' email",
|
||||
"custom_reply_to_email_description": "Use a different email address as the replyTo for confirmation emails instead of the organizer's email",
|
||||
"email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal.",
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { VerifyCodeDialog } from "@calcom/features/bookings/components/VerifyCodeDialog";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { TextField, Label } from "@calcom/ui/components/form";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import { useAddVerifiedEmail } from "../../../platform/atoms/event-types/hooks/useAddVerifiedEmail";
|
||||
import { useGetVerifiedEmails } from "../../../platform/atoms/event-types/hooks/useGetVerifiedEmails";
|
||||
import { useVerifyCode } from "../../../platform/atoms/hooks/useVerifyCode";
|
||||
import { useVerifyEmail } from "../../../platform/atoms/hooks/useVerifyEmail";
|
||||
|
||||
type AddVerifiedEmailProps = {
|
||||
username?: string;
|
||||
showToast: (message: string, variant: "success" | "warning" | "error") => void;
|
||||
};
|
||||
|
||||
const AddVerifiedEmail = ({ username, showToast }: AddVerifiedEmailProps) => {
|
||||
const { t } = useLocale();
|
||||
const [verifiedEmail, setVerifiedEmail] = useState("");
|
||||
const [isEmailVerificationModalVisible, setIsEmailVerificationModalVisible] = useState(false);
|
||||
const isValidEmail = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
||||
|
||||
const { refetch: refetchVerifiedEmails } = useGetVerifiedEmails();
|
||||
const { mutateAsync: addVerifiedEmail } = useAddVerifiedEmail({
|
||||
onSuccess: () => {
|
||||
refetchVerifiedEmails();
|
||||
showToast(t("email_verified"), "success");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("something_went_wrong"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
const verifyEmail = useVerifyEmail({
|
||||
email: verifiedEmail,
|
||||
name: username || t("there"),
|
||||
requiresBookerEmailVerification: true,
|
||||
});
|
||||
|
||||
const { handleVerifyEmail } = verifyEmail;
|
||||
|
||||
const verifyCode = useVerifyCode({
|
||||
onSuccess: async () => {
|
||||
setIsEmailVerificationModalVisible(false);
|
||||
setVerifiedEmail("");
|
||||
|
||||
await addVerifiedEmail({
|
||||
email: verifiedEmail,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="border-subtle border border-t-0 p-6">
|
||||
<Label
|
||||
className={classNames("text-emphasis mb-2 block text-sm font-medium leading-none")}
|
||||
htmlFor="add-verified-emails">
|
||||
{t("add_verified_emails")}
|
||||
</Label>
|
||||
<TextField
|
||||
id="add-verified-emails"
|
||||
containerClassName={classNames("w-full")}
|
||||
value={verifiedEmail}
|
||||
onChange={(e) => setVerifiedEmail(e.target.value)}
|
||||
data-testid="add-verified-emails"
|
||||
addOnSuffix={
|
||||
<Tooltip content={t("add_verified_email")}>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
size="sm"
|
||||
StartIcon="arrow-right"
|
||||
onClick={() => {
|
||||
if (!!verifiedEmail && isValidEmail(verifiedEmail)) {
|
||||
setIsEmailVerificationModalVisible(true);
|
||||
handleVerifyEmail();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<VerifyCodeDialog
|
||||
isOpenDialog={isEmailVerificationModalVisible}
|
||||
setIsOpenDialog={setIsEmailVerificationModalVisible}
|
||||
email={verifiedEmail}
|
||||
isUserSessionRequiredToVerify={false}
|
||||
verifyCodeWithSessionNotRequired={verifyCode.verifyCodeWithSessionNotRequired}
|
||||
verifyCodeWithSessionRequired={verifyCode.verifyCodeWithSessionRequired}
|
||||
error={verifyCode.error}
|
||||
resetErrors={verifyCode.resetErrors}
|
||||
isPending={verifyCode.isPending}
|
||||
setIsPending={verifyCode.setIsPending}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddVerifiedEmail;
|
||||
@@ -62,6 +62,7 @@ import {
|
||||
} from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import AddVerifiedEmail from "../../AddVerifiedEmail";
|
||||
import type { CustomEventTypeModalClassNames } from "./CustomEventTypeModal";
|
||||
import CustomEventTypeModal from "./CustomEventTypeModal";
|
||||
import type { EmailNotificationToggleCustomClassNames } from "./DisableAllEmailsSetting";
|
||||
@@ -508,7 +509,7 @@ export const EventAdvancedTab = ({
|
||||
"allowReschedulingCancelledBookings"
|
||||
);
|
||||
|
||||
const { isLocked, ...eventNameLocked } = shouldLockDisableProps("eventName");
|
||||
const { isLocked: _isLocked, ...eventNameLocked } = shouldLockDisableProps("eventName");
|
||||
|
||||
if (isManagedEventType) {
|
||||
multiplePrivateLinksLocked.disabled = true;
|
||||
@@ -1134,52 +1135,53 @@ export const EventAdvancedTab = ({
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{!isPlatform && (
|
||||
<>
|
||||
<Controller
|
||||
name="customReplyToEmail"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
labelClassName={classNames("text-sm", customClassNames?.customReplyToEmail?.label)}
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
|
||||
!!value && "rounded-b-none",
|
||||
customClassNames?.customReplyToEmail?.container
|
||||
)}
|
||||
descriptionClassName={customClassNames?.customReplyToEmail?.description}
|
||||
childrenClassName={classNames("lg:ml-0", customClassNames?.customReplyToEmail?.children)}
|
||||
title={t("custom_reply_to_email_title")}
|
||||
{...customReplyToEmailLocked}
|
||||
data-testid="custom-reply-to-email"
|
||||
description={t("custom_reply_to_email_description")}
|
||||
checked={!!customReplyToEmail}
|
||||
onCheckedChange={(e) => {
|
||||
onChange(
|
||||
e
|
||||
? customReplyToEmail || eventType.customReplyToEmail || verifiedEmails?.[0] || null
|
||||
: null
|
||||
);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<SelectField
|
||||
className="w-full"
|
||||
label={t("custom_reply_to_email_title")}
|
||||
required={!!customReplyToEmail}
|
||||
placeholder={t("select_verified_email")}
|
||||
data-testid="custom-reply-to-email-input"
|
||||
value={value ? { label: value, value } : undefined}
|
||||
onChange={(option) => onChange(option?.value || null)}
|
||||
options={verifiedEmails?.map((email) => ({ label: email, value: email })) || []}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Controller
|
||||
name="customReplyToEmail"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<SettingsToggle
|
||||
labelClassName={classNames("text-sm", customClassNames?.customReplyToEmail?.label)}
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle rounded-lg border py-6 px-4 sm:px-6",
|
||||
!!value && "rounded-b-none",
|
||||
customClassNames?.customReplyToEmail?.container
|
||||
)}
|
||||
descriptionClassName={customClassNames?.customReplyToEmail?.description}
|
||||
childrenClassName={classNames("lg:ml-0", customClassNames?.customReplyToEmail?.children)}
|
||||
title={t("custom_reply_to_email_title")}
|
||||
{...customReplyToEmailLocked}
|
||||
data-testid="custom-reply-to-email"
|
||||
description={t("custom_reply_to_email_description")}
|
||||
checked={!!customReplyToEmail}
|
||||
onCheckedChange={(e) => {
|
||||
onChange(
|
||||
e
|
||||
? customReplyToEmail || eventType.customReplyToEmail || verifiedEmails?.[0] || null
|
||||
: null
|
||||
);
|
||||
}}>
|
||||
{isPlatform && (
|
||||
<AddVerifiedEmail username={eventType.users[0]?.name || "there"} showToast={showToast} />
|
||||
)}
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<SelectField
|
||||
className="w-full"
|
||||
label={t("custom_reply_to_email_title")}
|
||||
required={!!customReplyToEmail}
|
||||
placeholder={t("select_verified_email")}
|
||||
data-testid="custom-reply-to-email-input"
|
||||
value={value ? { label: value, value } : undefined}
|
||||
onChange={(option) => onChange(option?.value || null)}
|
||||
options={verifiedEmails?.map((email) => ({ label: email, value: email })) || []}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
<Controller
|
||||
name="eventTypeColor"
|
||||
render={() => (
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiErrorResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../hooks/useAtomsContext";
|
||||
import { appendClientIdToEmail } from "../../lib/appendClientIdToEmail";
|
||||
import http from "../../lib/http";
|
||||
|
||||
interface IUseAddVerifiedEmail {
|
||||
onSuccess?: (res: ApiResponse) => void;
|
||||
onError?: (err: ApiErrorResponse | Error) => void;
|
||||
}
|
||||
|
||||
export const useAddVerifiedEmail = (
|
||||
{ onSuccess, onError }: IUseAddVerifiedEmail = {
|
||||
onSuccess: () => {
|
||||
return;
|
||||
},
|
||||
onError: () => {
|
||||
return;
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const { clientId } = useAtomsContext();
|
||||
|
||||
const verifiedEmailEntry = useMutation<
|
||||
ApiResponse<{
|
||||
status: string;
|
||||
data: {
|
||||
emailVerified: boolean;
|
||||
};
|
||||
}>,
|
||||
unknown,
|
||||
{
|
||||
email: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: (data) => {
|
||||
const { email } = data;
|
||||
const emailToSend = appendClientIdToEmail(email, clientId);
|
||||
|
||||
return http
|
||||
.post(`/atoms/emails/verified-emails`, {
|
||||
email: emailToSend,
|
||||
})
|
||||
.then((res) => {
|
||||
return res.data;
|
||||
});
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.status === SUCCESS_STATUS) {
|
||||
onSuccess?.(data);
|
||||
} else {
|
||||
onError?.(data);
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
onError?.(err as ApiErrorResponse);
|
||||
},
|
||||
});
|
||||
|
||||
return verifiedEmailEntry;
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse, ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../hooks/useAtomsContext";
|
||||
import http from "../../lib/http";
|
||||
|
||||
export const QUERY_KEY = "use-get-verified-emails";
|
||||
export const useGetVerifiedEmails = (teamId?: number) => {
|
||||
const pathname = `/atoms/emails/verified-emails${!!teamId ? `?teamId=${teamId}` : ""}`;
|
||||
const { isInit, accessToken } = useAtomsContext();
|
||||
|
||||
return useQuery({
|
||||
queryKey: [QUERY_KEY, teamId],
|
||||
queryFn: () => {
|
||||
return http?.get<ApiResponse<string[]>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return (res.data as ApiSuccessResponse<string[]>).data;
|
||||
}
|
||||
throw new Error(res.data.error.message);
|
||||
});
|
||||
},
|
||||
enabled: isInit && !!accessToken,
|
||||
staleTime: 5000,
|
||||
});
|
||||
};
|
||||
@@ -2,14 +2,18 @@ import type { EventAdvancedBaseProps } from "@calcom/features/eventtypes/compone
|
||||
import { EventAdvancedTab } from "@calcom/features/eventtypes/components/tabs/advanced/EventAdvancedTab";
|
||||
|
||||
import { useConnectedCalendars } from "../../hooks/useConnectedCalendars";
|
||||
import { useGetVerifiedEmails } from "../hooks/useGetVerifiedEmails";
|
||||
|
||||
const EventAdvancedPlatformWrapper = (props: EventAdvancedBaseProps) => {
|
||||
const { isPending, data: connectedCalendarsQuery, error } = useConnectedCalendars({});
|
||||
const { data: verifiedEmails } = useGetVerifiedEmails(props.team?.id);
|
||||
|
||||
return (
|
||||
<EventAdvancedTab
|
||||
{...props}
|
||||
calendarsQuery={{ data: connectedCalendarsQuery, isPending, error }}
|
||||
showBookerLayoutSelector={false}
|
||||
verifiedEmails={verifiedEmails}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function appendClientIdToEmail(email: string, clientId: string): string {
|
||||
const [localPart, domain] = email.split("@");
|
||||
return `${localPart}+${clientId}@${domain}`;
|
||||
}
|
||||
Reference in New Issue
Block a user