)}
@@ -184,6 +191,7 @@ export default function WorkflowDetailsPage(props: Props) {
setReload={setReload}
teamId={teamId}
readOnly={props.readOnly}
+ setKYCVerificationDialogOpen={setKYCVerificationDialogOpen}
/>
);
})}
@@ -212,6 +220,11 @@ export default function WorkflowDetailsPage(props: Props) {
setIsOpenDialog={setIsAddActionDialogOpen}
addAction={addAction}
/>
+
>;
teamId?: number;
readOnly: boolean;
+ setKYCVerificationDialogOpen: Dispatch>;
};
export default function WorkflowStepContainer(props: WorkflowStepProps) {
@@ -118,6 +120,11 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
const [showTimeSectionAfter, setShowTimeSectionAfter] = useState(
form.getValues("trigger") === WorkflowTriggerEvents.AFTER_EVENT
);
+
+ const [isRequiresConfirmationNeeded, setIsRequiresConfirmationNeeded] = useState(
+ isTextMessageToAttendeeAction(step?.action)
+ );
+
const { data: actionOptions } = trpc.viewer.workflows.getWorkflowActionOptions.useQuery();
const triggerOptions = getWorkflowTriggerOptions(t);
const templateOptions = getWorkflowTemplateOptions(t, step?.action);
@@ -331,6 +338,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: step.action,
needsUpgrade: false,
+ needsVerification: false,
+ verificationAction: () => props.setKYCVerificationDialogOpen(true),
};
const selectedTemplate = { label: t(`${step.template.toLowerCase()}`), value: step.template };
@@ -448,6 +457,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
setIsEmailSubjectNeeded(true);
}
+ if (isTextMessageToAttendeeAction(val.value)) {
+ setIsRequiresConfirmationNeeded(true);
+ } else {
+ setIsRequiresConfirmationNeeded(false);
+ }
+
if (
form.getValues(`steps.${step.stepNumber - 1}.template`) ===
WorkflowTemplates.REMINDER
@@ -513,16 +528,28 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}
}}
defaultValue={selectedAction}
- options={actionOptions}
+ options={actionOptions?.map((option) => ({
+ ...option,
+ verificationAction: () => props.setKYCVerificationDialogOpen(true),
+ }))}
isOptionDisabled={(option: {
label: string;
value: WorkflowActions;
needsUpgrade: boolean;
- }) => option.needsUpgrade}
+ needsVerification: boolean;
+ }) => option.needsUpgrade || option.needsVerification}
/>
);
}}
/>
+ {isRequiresConfirmationNeeded ? (
+
+
+
{t("requires_confirmation_mandatory")}
+
+ ) : (
+ <>>
+ )}
{isPhoneNumberNeeded && (
diff --git a/packages/features/ee/workflows/lib/actionHelperFunctions.ts b/packages/features/ee/workflows/lib/actionHelperFunctions.ts
index fc24562993..401a6b5875 100644
--- a/packages/features/ee/workflows/lib/actionHelperFunctions.ts
+++ b/packages/features/ee/workflows/lib/actionHelperFunctions.ts
@@ -38,6 +38,10 @@ export function isAttendeeAction(action: WorkflowActions) {
);
}
+export function isTextMessageToAttendeeAction(action?: WorkflowActions) {
+ return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
+}
+
export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): WorkflowTemplates {
switch (trigger) {
case "NEW_EVENT":
diff --git a/packages/features/ee/workflows/lib/getOptions.ts b/packages/features/ee/workflows/lib/getOptions.ts
index 7dd3c0ee9f..ccaa39a288 100644
--- a/packages/features/ee/workflows/lib/getOptions.ts
+++ b/packages/features/ee/workflows/lib/getOptions.ts
@@ -2,7 +2,11 @@ import type { TFunction } from "next-i18next";
import { WorkflowActions } from "@calcom/prisma/enums";
-import { isSMSOrWhatsappAction, isWhatsappAction } from "./actionHelperFunctions";
+import {
+ isTextMessageToAttendeeAction,
+ isSMSOrWhatsappAction,
+ isWhatsappAction,
+} from "./actionHelperFunctions";
import {
TIME_UNIT,
WHATSAPP_WORKFLOW_TEMPLATES,
@@ -11,7 +15,7 @@ import {
WORKFLOW_TRIGGER_EVENTS,
} from "./constants";
-export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) {
+export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean, isKYCVerified?: boolean) {
return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode
.map((action) => {
const actionString = t(`${action.toLowerCase()}_action`);
@@ -20,6 +24,7 @@ export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: action,
needsUpgrade: isSMSOrWhatsappAction(action) && !isTeamsPlan,
+ needsVerification: isTextMessageToAttendeeAction(action) && !isKYCVerified,
};
});
}
diff --git a/packages/features/ee/workflows/lib/isEventTypeOwnerKYCVerified.ts b/packages/features/ee/workflows/lib/isEventTypeOwnerKYCVerified.ts
new file mode 100644
index 0000000000..436494f71f
--- /dev/null
+++ b/packages/features/ee/workflows/lib/isEventTypeOwnerKYCVerified.ts
@@ -0,0 +1,45 @@
+import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
+import type { Prisma } from "@calcom/prisma/client";
+
+type EventTypeOwnerType = {
+ team?: {
+ metadata: Prisma.JsonValue;
+ } | null;
+ owner?: {
+ metadata: Prisma.JsonValue;
+ teams: {
+ accepted: boolean;
+ team: {
+ metadata: Prisma.JsonValue;
+ };
+ }[];
+ } | null;
+};
+
+export function isEventTypeOwnerKYCVerified(eventType?: EventTypeOwnerType | null) {
+ if (!eventType) return false;
+
+ if (eventType.team) {
+ const isKYCVerified =
+ eventType.team &&
+ hasKeyInMetadata(eventType.team, "kycVerified") &&
+ !!eventType.team.metadata.kycVerified;
+ return isKYCVerified;
+ }
+
+ if (eventType.owner) {
+ const isKYCVerified =
+ eventType.owner &&
+ hasKeyInMetadata(eventType.owner, "kycVerified") &&
+ !!eventType.owner.metadata.kycVerified;
+ if (isKYCVerified) return isKYCVerified;
+
+ const isPartOfVerifiedTeam = eventType.owner.teams.find(
+ (team) =>
+ team.accepted && hasKeyInMetadata(team.team, "kycVerified") && !!team.team.metadata.kycVerified
+ );
+ return !!isPartOfVerifiedTeam;
+ }
+
+ return false;
+}
diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
index 746ea7423f..d3dffc9bdf 100644
--- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
+++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts
@@ -1,6 +1,9 @@
import type { Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@prisma/client";
-import { isWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
+import {
+ isTextMessageToAttendeeAction,
+ isWhatsappAction,
+} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -20,6 +23,8 @@ type ProcessWorkflowStepParams = {
emailAttendeeSendToOverride?: string;
hideBranding?: boolean;
seatReferenceUid?: string;
+ isKYCVerified: boolean;
+ eventTypeRequiresConfirmation?: boolean;
};
export interface ScheduleWorkflowRemindersArgs extends ProcessWorkflowStepParams {
@@ -28,7 +33,7 @@ export interface ScheduleWorkflowRemindersArgs extends ProcessWorkflowStepParams
steps: WorkflowStep[];
};
})[];
- requiresConfirmation?: boolean;
+ isNotConfirmed?: boolean;
isRescheduleEvent?: boolean;
isFirstRecurringEvent?: boolean;
}
@@ -42,8 +47,13 @@ const processWorkflowStep = async (
emailAttendeeSendToOverride,
hideBranding,
seatReferenceUid,
+ eventTypeRequiresConfirmation,
+ isKYCVerified,
}: ProcessWorkflowStepParams
) => {
+ if (isTextMessageToAttendeeAction(step.action) && (!isKYCVerified || !eventTypeRequiresConfirmation))
+ return;
+
if (step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER) {
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
await scheduleSMSReminder(
@@ -125,15 +135,16 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
workflows,
smsReminderNumber,
calendarEvent: evt,
- requiresConfirmation = false,
+ isNotConfirmed = false,
isRescheduleEvent = false,
isFirstRecurringEvent = false,
emailAttendeeSendToOverride = "",
hideBranding,
seatReferenceUid,
+ eventTypeRequiresConfirmation = false,
+ isKYCVerified,
} = args;
-
- if (requiresConfirmation || !workflows.length) return;
+ if (isNotConfirmed || !workflows.length) return;
for (const workflowReference of workflows) {
if (workflowReference.workflow.steps.length === 0) continue;
@@ -164,6 +175,8 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
smsReminderNumber,
hideBranding,
seatReferenceUid,
+ eventTypeRequiresConfirmation,
+ isKYCVerified,
});
}
}
@@ -194,10 +207,13 @@ export interface SendCancelledRemindersArgs {
smsReminderNumber: string | null;
evt: ExtendedCalendarEvent;
hideBranding?: boolean;
+ isKYCVerified: boolean;
+ eventTypeRequiresConfirmation?: boolean;
}
export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
- const { workflows, smsReminderNumber, evt, hideBranding } = args;
+ const { workflows, smsReminderNumber, evt, hideBranding, isKYCVerified, eventTypeRequiresConfirmation } =
+ args;
if (!workflows.length) return;
for (const workflowRef of workflows) {
@@ -210,6 +226,8 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
smsReminderNumber,
hideBranding,
calendarEvent: evt,
+ eventTypeRequiresConfirmation,
+ isKYCVerified,
});
}
}
diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
index fbfb2c0356..faf3ff58d5 100644
--- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
@@ -162,7 +162,7 @@ export const scheduleSMSReminder = async (
// Allows debugging generated email content without waiting for sendgrid to send emails
log.debug(`Sending sms for trigger ${triggerEvent}`, message);
- if (message.length > 0 && reminderPhone && isNumberVerified && action !== WorkflowActions.SMS_ATTENDEE) {
+ if (message.length > 0 && reminderPhone && isNumberVerified) {
//send SMS when event is booked/cancelled/rescheduled
if (
triggerEvent === WorkflowTriggerEvents.NEW_EVENT ||
diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
index acb8f9f61f..51f28be26a 100644
--- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
+++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts
@@ -141,12 +141,7 @@ export const scheduleWhatsappReminder = async (
// Allows debugging generated whatsapp content without waiting for twilio to send whatsapp messages
log.debug(`Sending Whatsapp for trigger ${triggerEvent}`, message);
- if (
- message.length > 0 &&
- reminderPhone &&
- isNumberVerified &&
- action !== WorkflowActions.WHATSAPP_ATTENDEE
- ) {
+ if (message.length > 0 && reminderPhone && isNumberVerified) {
//send WHATSAPP when event is booked/cancelled/rescheduled
if (
triggerEvent === WorkflowTriggerEvents.NEW_EVENT ||
diff --git a/packages/features/settings/layouts/SettingsLayout.tsx b/packages/features/settings/layouts/SettingsLayout.tsx
index 57857ee354..325b26d73b 100644
--- a/packages/features/settings/layouts/SettingsLayout.tsx
+++ b/packages/features/settings/layouts/SettingsLayout.tsx
@@ -118,6 +118,7 @@ const tabs: VerticalTabItemProps[] = [
{ name: "apps", href: "/settings/admin/apps/calendar" },
{ name: "users", href: "/settings/admin/users" },
{ name: "organizations", href: "/settings/admin/organizations" },
+ { name: "kyc_verification", href: "/settings/admin/kycVerification" },
],
},
];
diff --git a/packages/trpc/react/trpc.ts b/packages/trpc/react/trpc.ts
index 044bbb44d8..5382c9c3fd 100644
--- a/packages/trpc/react/trpc.ts
+++ b/packages/trpc/react/trpc.ts
@@ -40,6 +40,7 @@ const ENDPOINTS = [
"workflows",
"appsRouter",
"googleWorkspace",
+ "kycVerification",
] as const;
export type Endpoint = (typeof ENDPOINTS)[number];
diff --git a/packages/trpc/server/routers/viewer/_router.tsx b/packages/trpc/server/routers/viewer/_router.tsx
index 1152175223..332d85d62c 100644
--- a/packages/trpc/server/routers/viewer/_router.tsx
+++ b/packages/trpc/server/routers/viewer/_router.tsx
@@ -15,6 +15,7 @@ import { bookingsRouter } from "./bookings/_router";
import { deploymentSetupRouter } from "./deploymentSetup/_router";
import { eventTypesRouter } from "./eventTypes/_router";
import { googleWorkspaceRouter } from "./googleWorkspace/_router";
+import { kycVerificationRouter } from "./kycVerification/_router";
import { viewerOrganizationsRouter } from "./organizations/_router";
import { paymentsRouter } from "./payments/_router";
import { slotsRouter } from "./slots/_router";
@@ -50,5 +51,6 @@ export const viewerRouter = mergeRouters(
users: userAdminRouter,
googleWorkspace: googleWorkspaceRouter,
admin: adminRouter,
+ kycVerification: kycVerificationRouter,
})
);
diff --git a/packages/trpc/server/routers/viewer/kycVerification/_router.tsx b/packages/trpc/server/routers/viewer/kycVerification/_router.tsx
new file mode 100644
index 0000000000..f867ae5cc5
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/kycVerification/_router.tsx
@@ -0,0 +1,42 @@
+import { authedAdminProcedure } from "../../../procedures/authedProcedure";
+import { router } from "../../../trpc";
+import { ZVerifyInputSchema } from "./verify.schema";
+
+type KYCVerificationRouterHandlerCache = {
+ isVerified?: typeof import("./isVerified.handler").isVerifiedHandler;
+ verify?: typeof import("./verify.handler").verifyHandler;
+};
+
+const UNSTABLE_HANDLER_CACHE: KYCVerificationRouterHandlerCache = {};
+
+export const kycVerificationRouter = router({
+ isVerified: authedAdminProcedure.query(async ({ ctx }) => {
+ if (!UNSTABLE_HANDLER_CACHE.isVerified) {
+ UNSTABLE_HANDLER_CACHE.isVerified = await import("./isVerified.handler").then(
+ (mod) => mod.isVerifiedHandler
+ );
+ }
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.isVerified) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.isVerified({
+ ctx,
+ });
+ }),
+ verify: authedAdminProcedure.input(ZVerifyInputSchema).mutation(async ({ ctx, input }) => {
+ if (!UNSTABLE_HANDLER_CACHE.verify) {
+ UNSTABLE_HANDLER_CACHE.verify = await import("./verify.handler").then((mod) => mod.verifyHandler);
+ }
+ // Unreachable code but required for type safety
+ if (!UNSTABLE_HANDLER_CACHE.verify) {
+ throw new Error("Failed to load handler");
+ }
+
+ return UNSTABLE_HANDLER_CACHE.verify({
+ ctx,
+ input,
+ });
+ }),
+});
diff --git a/packages/trpc/server/routers/viewer/kycVerification/isVerified.handler.ts b/packages/trpc/server/routers/viewer/kycVerification/isVerified.handler.ts
new file mode 100644
index 0000000000..8cd87833ca
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/kycVerification/isVerified.handler.ts
@@ -0,0 +1,44 @@
+import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
+import { prisma } from "@calcom/prisma";
+import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
+
+type IsKYCVerifiedOptions = {
+ ctx: {
+ user: NonNullable;
+ };
+};
+
+export const isVerifiedHandler = async ({ ctx }: IsKYCVerifiedOptions) => {
+ const user = ctx.user;
+
+ const memberships = await prisma.membership.findMany({
+ where: {
+ accepted: true,
+ userId: user.id,
+ team: {
+ slug: {
+ not: null,
+ },
+ },
+ },
+ select: {
+ team: {
+ select: {
+ metadata: true,
+ },
+ },
+ },
+ });
+
+ let isKYCVerified = user && hasKeyInMetadata(user, "kycVerified") ? !!user.metadata.kycVerified : false;
+
+ if (!isKYCVerified) {
+ //check if user is part of a team that is KYC verified
+ isKYCVerified = !!memberships.find(
+ (membership) =>
+ hasKeyInMetadata(membership.team, "kycVerified") && !!membership.team.metadata.kycVerified
+ );
+ }
+
+ return { isKYCVerified };
+};
diff --git a/packages/trpc/server/routers/viewer/kycVerification/isVerified.schema.ts b/packages/trpc/server/routers/viewer/kycVerification/isVerified.schema.ts
new file mode 100644
index 0000000000..cb0ff5c3b5
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/kycVerification/isVerified.schema.ts
@@ -0,0 +1 @@
+export {};
diff --git a/packages/trpc/server/routers/viewer/kycVerification/verify.handler.ts b/packages/trpc/server/routers/viewer/kycVerification/verify.handler.ts
new file mode 100644
index 0000000000..65fdbe9f3f
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/kycVerification/verify.handler.ts
@@ -0,0 +1,76 @@
+import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
+import { prisma } from "@calcom/prisma";
+import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
+
+import type { TVerifyInputSchema } from "./verify.schema";
+
+type VerifyOptions = {
+ ctx: {
+ user: NonNullable;
+ };
+ input: TVerifyInputSchema;
+};
+
+export const verifyHandler = async ({ ctx, input }: VerifyOptions) => {
+ const { name, isTeam } = input;
+ if (isTeam) {
+ const team = await prisma.team.findFirst({
+ where: {
+ slug: name,
+ },
+ });
+ if (!team) {
+ throw new Error("Team not found");
+ }
+
+ if (hasKeyInMetadata(team, "kycVerified") && !!team.metadata.kycVerified) {
+ throw new Error("Team already verified");
+ }
+
+ const metadata = typeof team.metadata === "object" ? team.metadata : {};
+ const updatedMetadata = { ...metadata, kycVerified: true };
+
+ const updatedTeam = await prisma.team.update({
+ where: {
+ id: team.id,
+ },
+ data: {
+ metadata: updatedMetadata,
+ },
+ });
+ return {
+ name: updatedTeam.slug,
+ isTeam: true,
+ };
+ }
+
+ const user = await prisma.user.findFirst({
+ where: {
+ username: name,
+ },
+ });
+
+ if (!user) {
+ throw new Error("User not found");
+ }
+
+ if (hasKeyInMetadata(user, "kycVerified") && !!user.metadata.kycVerified) {
+ throw new Error("User already verified");
+ }
+
+ const metadata = typeof user.metadata === "object" ? user.metadata : {};
+ const updatedMetadata = { ...metadata, kycVerified: true };
+
+ const updatedUser = await prisma.user.update({
+ where: {
+ id: user.id,
+ },
+ data: {
+ metadata: updatedMetadata,
+ },
+ });
+ return {
+ name: updatedUser.username,
+ isTeam: false,
+ };
+};
diff --git a/packages/trpc/server/routers/viewer/kycVerification/verify.schema.ts b/packages/trpc/server/routers/viewer/kycVerification/verify.schema.ts
new file mode 100644
index 0000000000..dde3d65234
--- /dev/null
+++ b/packages/trpc/server/routers/viewer/kycVerification/verify.schema.ts
@@ -0,0 +1,8 @@
+import { z } from "zod";
+
+export const ZVerifyInputSchema = z.object({
+ name: z.string(),
+ isTeam: z.boolean(),
+});
+
+export type TVerifyInputSchema = z.infer;
diff --git a/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts b/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts
index a4159afcf3..0c113999ac 100644
--- a/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts
+++ b/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts
@@ -4,6 +4,7 @@ import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { getTranslation } from "@calcom/lib/server/i18n";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
+import { isVerifiedHandler } from "../kycVerification/isVerified.handler";
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
type GetWorkflowActionOptionsOptions = {
@@ -25,6 +26,13 @@ export const getWorkflowActionOptionsHandler = async ({ ctx }: GetWorkflowAction
const { hasTeamPlan } = await hasTeamPlanHandler({ ctx });
isTeamsPlan = !!hasTeamPlan;
}
+
+ const { isKYCVerified } = await isVerifiedHandler({ ctx });
+
const t = await getTranslation(ctx.user.locale, "common");
- return getWorkflowActionOptions(t, IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan);
+ return getWorkflowActionOptions(
+ t,
+ IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan,
+ isKYCVerified
+ );
};
diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts
index af4f0bb5d2..2ee1a70d96 100644
--- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts
+++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts
@@ -1,6 +1,9 @@
import type { Prisma } from "@prisma/client";
-import { isSMSOrWhatsappAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
+import {
+ isSMSOrWhatsappAction,
+ isTextMessageToAttendeeAction,
+} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
import {
deleteScheduledEmailReminder,
scheduleEmailReminder,
@@ -22,6 +25,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
+import { isVerifiedHandler } from "../kycVerification/isVerified.handler";
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
import type { TUpdateInputSchema } from "./update.schema";
import {
@@ -80,6 +84,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;
+ const kycVerified = await isVerifiedHandler({ ctx });
+ const isKYCVerified = kycVerified.isKYCVerified;
+
const activeOnEventTypes = await ctx.prisma.eventType.findMany({
where: {
id: {
@@ -421,6 +428,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
if (!hasPaidPlan && !isSMSOrWhatsappAction(oldStep.action) && isSMSOrWhatsappAction(newStep.action)) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
+ if (!isKYCVerified && isTextMessageToAttendeeAction(newStep.action)) {
+ throw new TRPCError({ code: "UNAUTHORIZED", message: "Account needs to be verified" });
+ }
const requiresSender =
newStep.action === WorkflowActions.SMS_NUMBER || newStep.action === WorkflowActions.WHATSAPP_NUMBER;
await ctx.prisma.workflowStep.update({
@@ -593,6 +603,9 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
if (isSMSOrWhatsappAction(s.action) && !hasPaidPlan) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
+ if (isTextMessageToAttendeeAction(s.action)) {
+ throw new TRPCError({ code: "UNAUTHORIZED", message: "Account needs to be verified" });
+ }
const { id: _stepId, ...stepToAdd } = s;
return stepToAdd;
}
diff --git a/packages/ui/components/badge/KYCVerificationBadge.tsx b/packages/ui/components/badge/KYCVerificationBadge.tsx
new file mode 100644
index 0000000000..fbbcdf8bd2
--- /dev/null
+++ b/packages/ui/components/badge/KYCVerificationBadge.tsx
@@ -0,0 +1,20 @@
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
+import { Tooltip } from "../tooltip";
+import { Badge } from "./Badge";
+
+export const KYCVerificationBadge = function KYCVerificationBadge(props: { verifyTeamAction?: () => void }) {
+ const { verifyTeamAction } = props;
+ const { t } = useLocale();
+ if (!verifyTeamAction) return <>>;
+
+ return (
+ <>
+
+ verifyTeamAction()}>
+ {t("verify_account")}
+
+
+ >
+ );
+};
diff --git a/packages/ui/components/badge/index.ts b/packages/ui/components/badge/index.ts
index bd0decbf3d..4c6fc4ac3a 100644
--- a/packages/ui/components/badge/index.ts
+++ b/packages/ui/components/badge/index.ts
@@ -1,4 +1,5 @@
export { Badge } from "./Badge";
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
+export { KYCVerificationBadge } from "./KYCVerificationBadge";
export type { BadgeProps } from "./Badge";
diff --git a/packages/ui/components/form/select/components.tsx b/packages/ui/components/form/select/components.tsx
index f555f36b69..3f91edc91e 100644
--- a/packages/ui/components/form/select/components.tsx
+++ b/packages/ui/components/form/select/components.tsx
@@ -3,7 +3,7 @@ import { components as reactSelectComponents } from "react-select";
import { classNames } from "@calcom/lib";
-import { UpgradeTeamsBadge } from "../../badge";
+import { UpgradeTeamsBadge, KYCVerificationBadge } from "../../badge";
import { Check } from "../../icon";
export const InputComponent = <
@@ -30,6 +30,8 @@ type ExtendedOption = {
value: string | number;
label: string;
needsUpgrade?: boolean;
+ needsVerification?: boolean;
+ verificationAction?: () => void;
};
export const OptionComponent = <
@@ -46,7 +48,15 @@ export const OptionComponent = <
{props.label || <> >}
- {(props.data as unknown as ExtendedOption).needsUpgrade && }
+ {(props.data as unknown as ExtendedOption).needsUpgrade ? (
+
+ ) : (props.data as unknown as ExtendedOption).needsVerification ? (
+
+ ) : (
+ <>>
+ )}
{props.isSelected && }