fix: remove kyc and make text messages to attendee org only (#11389)
* add orgs upgrade badge to select * don't allow updating to if no teams or orgs plan * make sure only paying users can request verification codes * add new action helper function * remove kyc code * code clean up * fix verify button UI * fix type errors * add eventTypeRequiresConfirmation everywhere * address feedback --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com>pull/11473/head
parent
96263b0cf7
commit
3b50fe075d
|
@ -1,4 +0,0 @@
|
||||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
|
||||||
import { kycVerificationRouter } from "@calcom/trpc/server/routers/viewer/kycVerification/_router";
|
|
||||||
|
|
||||||
export default createNextApiHandler(kycVerificationRouter);
|
|
|
@ -1,11 +0,0 @@
|
||||||
import PageWrapper from "@components/PageWrapper";
|
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
|
||||||
|
|
||||||
import KYCVerificationView from "./kycVerificationView";
|
|
||||||
|
|
||||||
const KYCVerificationPage = () => <KYCVerificationView />;
|
|
||||||
|
|
||||||
KYCVerificationPage.getLayout = getLayout;
|
|
||||||
KYCVerificationPage.PageWrapper = PageWrapper;
|
|
||||||
|
|
||||||
export default KYCVerificationPage;
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc";
|
|
||||||
import { Meta, Form, Button, TextField, showToast } from "@calcom/ui";
|
|
||||||
|
|
||||||
type FormValues = {
|
|
||||||
name?: string | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function KYCVerificationView() {
|
|
||||||
const teamForm = useForm<FormValues>();
|
|
||||||
const userForm = useForm<FormValues>();
|
|
||||||
|
|
||||||
const mutation = trpc.viewer.kycVerification.verify.useMutation({
|
|
||||||
onSuccess: async (data) => {
|
|
||||||
showToast(`Successfully verified ${data.isTeam ? "team" : "user"} ${data.name}`, "success");
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
|
||||||
showToast(`Verification failed: ${error.message}`, "error");
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Meta
|
|
||||||
title="KYC Verification"
|
|
||||||
description="Here you can verify users and teams. This verification is needed for sending sms/whatsapp messages to attendees."
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 font-medium">Verify Team</div>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
form={teamForm}
|
|
||||||
handleSubmit={(values) => {
|
|
||||||
mutation.mutate({
|
|
||||||
name: values.name || "",
|
|
||||||
isTeam: true,
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<TextField
|
|
||||||
{...teamForm.register("name")}
|
|
||||||
label=""
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
placeholder="team slug"
|
|
||||||
className="-mt-2 "
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button type="submit">Verify</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="mb-2 mt-6 font-medium">Verify User</div>
|
|
||||||
<Form
|
|
||||||
form={userForm}
|
|
||||||
handleSubmit={(values) => {
|
|
||||||
mutation.mutate({
|
|
||||||
name: values.name || "",
|
|
||||||
isTeam: false,
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<TextField
|
|
||||||
{...userForm.register("name")}
|
|
||||||
label=""
|
|
||||||
type="text"
|
|
||||||
id="name"
|
|
||||||
placeholder="user name"
|
|
||||||
className="-mt-2"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Button type="submit">Verify</Button>
|
|
||||||
</div>
|
|
||||||
</Form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1639,6 +1639,7 @@
|
||||||
"minimum_round_robin_hosts_count": "Number of hosts required to attend",
|
"minimum_round_robin_hosts_count": "Number of hosts required to attend",
|
||||||
"hosts": "Hosts",
|
"hosts": "Hosts",
|
||||||
"upgrade_to_enable_feature": "You need to create a team to enable this feature. Click to create a team.",
|
"upgrade_to_enable_feature": "You need to create a team to enable this feature. Click to create a team.",
|
||||||
|
"orgs_upgrade_to_enable_feature" : "You need to upgrade to our enterprise plan to enable this feature.",
|
||||||
"new_attendee": "New Attendee",
|
"new_attendee": "New Attendee",
|
||||||
"awaiting_approval": "Awaiting Approval",
|
"awaiting_approval": "Awaiting Approval",
|
||||||
"requires_google_calendar": "This app requires a Google Calendar connection",
|
"requires_google_calendar": "This app requires a Google Calendar connection",
|
||||||
|
@ -2003,11 +2004,6 @@
|
||||||
"requires_booker_email_verification": "Requires booker email verification",
|
"requires_booker_email_verification": "Requires booker email verification",
|
||||||
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
||||||
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
||||||
"kyc_verification_information": "To ensure security, you have to verify your {{teamOrAccount}} before sending text messages to attendees. Please contact us at <a>{{supportEmail}}</a> and provide the following information:",
|
|
||||||
"kyc_verification_documents": "<ul><li>Your {{teamOrUser}}</li><li>For businesses: Attach your Business Verification Documentation</li><li>For individuals: Attach a government-issued ID</li></ul>",
|
|
||||||
"verify_team_or_account": "Verify {{teamOrAccount}}",
|
|
||||||
"verify_account": "Verify Account",
|
|
||||||
"kyc_verification": "KYC Verification",
|
|
||||||
"organizations": "Organizations",
|
"organizations": "Organizations",
|
||||||
"org_admin_other_teams": "Other teams",
|
"org_admin_other_teams": "Other teams",
|
||||||
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
|
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
|
||||||
|
|
|
@ -9,7 +9,6 @@ import { deleteMeeting, updateMeeting } from "@calcom/core/videoClient";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendCancelledEmails, sendCancelledSeatEmails } from "@calcom/emails";
|
import { sendCancelledEmails, sendCancelledSeatEmails } from "@calcom/emails";
|
||||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
import { isEventTypeOwnerKYCVerified } from "@calcom/features/ee/workflows/lib/isEventTypeOwnerKYCVerified";
|
|
||||||
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
||||||
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||||
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
||||||
|
@ -71,17 +70,6 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
metadata: true,
|
|
||||||
teams: {
|
|
||||||
select: {
|
|
||||||
accepted: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
teamId: true,
|
teamId: true,
|
||||||
|
@ -299,8 +287,6 @@ async function handler(req: CustomRequest) {
|
||||||
);
|
);
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
const isKYCVerified = isEventTypeOwnerKYCVerified(bookingToDelete.eventType);
|
|
||||||
|
|
||||||
//Workflows - schedule reminders
|
//Workflows - schedule reminders
|
||||||
if (bookingToDelete.eventType?.workflows) {
|
if (bookingToDelete.eventType?.workflows) {
|
||||||
await sendCancelledReminders({
|
await sendCancelledReminders({
|
||||||
|
@ -311,7 +297,7 @@ async function handler(req: CustomRequest) {
|
||||||
...{ eventType: { slug: bookingToDelete.eventType.slug } },
|
...{ eventType: { slug: bookingToDelete.eventType.slug } },
|
||||||
},
|
},
|
||||||
hideBranding: !!bookingToDelete.eventType.owner?.hideBranding,
|
hideBranding: !!bookingToDelete.eventType.owner?.hideBranding,
|
||||||
isKYCVerified,
|
eventTypeRequiresConfirmation: bookingToDelete.eventType.requiresConfirmation,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import type { Prisma, Workflow, WorkflowsOnEventTypes, WorkflowStep } from "@pri
|
||||||
import type { EventManagerUser } from "@calcom/core/EventManager";
|
import type { EventManagerUser } from "@calcom/core/EventManager";
|
||||||
import EventManager from "@calcom/core/EventManager";
|
import EventManager from "@calcom/core/EventManager";
|
||||||
import { sendScheduledEmails } from "@calcom/emails";
|
import { sendScheduledEmails } from "@calcom/emails";
|
||||||
import { isEventTypeOwnerKYCVerified } from "@calcom/features/ee/workflows/lib/isEventTypeOwnerKYCVerified";
|
|
||||||
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||||
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
|
import { scheduleTrigger } from "@calcom/features/webhooks/lib/scheduleTrigger";
|
||||||
|
@ -87,18 +86,8 @@ export async function handleConfirmation(args: {
|
||||||
eventType: {
|
eventType: {
|
||||||
bookingFields: Prisma.JsonValue | null;
|
bookingFields: Prisma.JsonValue | null;
|
||||||
slug: string;
|
slug: string;
|
||||||
team: {
|
|
||||||
metadata: Prisma.JsonValue;
|
|
||||||
} | null;
|
|
||||||
owner: {
|
owner: {
|
||||||
hideBranding?: boolean | null;
|
hideBranding?: boolean | null;
|
||||||
metadata: Prisma.JsonValue;
|
|
||||||
teams: {
|
|
||||||
accepted: boolean;
|
|
||||||
team: {
|
|
||||||
metadata: Prisma.JsonValue;
|
|
||||||
};
|
|
||||||
}[];
|
|
||||||
} | null;
|
} | null;
|
||||||
workflows: (WorkflowsOnEventTypes & {
|
workflows: (WorkflowsOnEventTypes & {
|
||||||
workflow: Workflow & {
|
workflow: Workflow & {
|
||||||
|
@ -135,25 +124,9 @@ export async function handleConfirmation(args: {
|
||||||
select: {
|
select: {
|
||||||
slug: true,
|
slug: true,
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
owner: {
|
owner: {
|
||||||
select: {
|
select: {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
metadata: true,
|
|
||||||
teams: {
|
|
||||||
select: {
|
|
||||||
accepted: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
|
@ -202,25 +175,9 @@ export async function handleConfirmation(args: {
|
||||||
select: {
|
select: {
|
||||||
slug: true,
|
slug: true,
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
owner: {
|
owner: {
|
||||||
select: {
|
select: {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
metadata: true,
|
|
||||||
teams: {
|
|
||||||
select: {
|
|
||||||
accepted: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
|
@ -250,8 +207,6 @@ export async function handleConfirmation(args: {
|
||||||
updatedBookings.push(updatedBooking);
|
updatedBookings.push(updatedBooking);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isKYCVerified = isEventTypeOwnerKYCVerified(updatedBookings[0].eventType);
|
|
||||||
|
|
||||||
//Workflows - set reminders for confirmed events
|
//Workflows - set reminders for confirmed events
|
||||||
try {
|
try {
|
||||||
for (let index = 0; index < updatedBookings.length; index++) {
|
for (let index = 0; index < updatedBookings.length; index++) {
|
||||||
|
@ -276,7 +231,6 @@ export async function handleConfirmation(args: {
|
||||||
isFirstRecurringEvent: isFirstBooking,
|
isFirstRecurringEvent: isFirstBooking,
|
||||||
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
|
hideBranding: !!updatedBookings[index].eventType?.owner?.hideBranding,
|
||||||
eventTypeRequiresConfirmation: true,
|
eventTypeRequiresConfirmation: true,
|
||||||
isKYCVerified,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,7 +39,6 @@ import {
|
||||||
allowDisablingAttendeeConfirmationEmails,
|
allowDisablingAttendeeConfirmationEmails,
|
||||||
allowDisablingHostConfirmationEmails,
|
allowDisablingHostConfirmationEmails,
|
||||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||||
import { isEventTypeOwnerKYCVerified } from "@calcom/features/ee/workflows/lib/isEventTypeOwnerKYCVerified";
|
|
||||||
import {
|
import {
|
||||||
cancelWorkflowReminders,
|
cancelWorkflowReminders,
|
||||||
scheduleWorkflowReminders,
|
scheduleWorkflowReminders,
|
||||||
|
@ -260,7 +259,6 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
metadata: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
bookingFields: true,
|
bookingFields: true,
|
||||||
|
@ -292,17 +290,6 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||||
owner: {
|
owner: {
|
||||||
select: {
|
select: {
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
metadata: true,
|
|
||||||
teams: {
|
|
||||||
select: {
|
|
||||||
accepted: true,
|
|
||||||
team: {
|
|
||||||
select: {
|
|
||||||
metadata: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
|
@ -379,7 +366,7 @@ async function ensureAvailableUsers(
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const availableUsers: IsFixedAwareUser[] = [];
|
const availableUsers: IsFixedAwareUser[] = [];
|
||||||
const duration = dayjs(input.dateTo).diff(input.dateFrom, 'minute');
|
const duration = dayjs(input.dateTo).diff(input.dateFrom, "minute");
|
||||||
|
|
||||||
const originalBookingDuration = input.originalRescheduledBooking
|
const originalBookingDuration = input.originalRescheduledBooking
|
||||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||||
|
@ -1199,8 +1186,6 @@ async function handler(
|
||||||
|
|
||||||
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
|
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
|
||||||
|
|
||||||
const isKYCVerified = isEventTypeOwnerKYCVerified(eventType);
|
|
||||||
|
|
||||||
const handleSeats = async () => {
|
const handleSeats = async () => {
|
||||||
let resultBooking:
|
let resultBooking:
|
||||||
| (Partial<Booking> & {
|
| (Partial<Booking> & {
|
||||||
|
@ -1769,7 +1754,7 @@ async function handler(
|
||||||
isFirstRecurringEvent: true,
|
isFirstRecurringEvent: true,
|
||||||
emailAttendeeSendToOverride: bookerEmail,
|
emailAttendeeSendToOverride: bookerEmail,
|
||||||
seatReferenceUid: evt.attendeeSeatId,
|
seatReferenceUid: evt.attendeeSeatId,
|
||||||
isKYCVerified,
|
eventTypeRequiresConfirmation: eventType.requiresConfirmation,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error while scheduling workflow reminders", error);
|
log.error("Error while scheduling workflow reminders", error);
|
||||||
|
@ -2416,7 +2401,7 @@ async function handler(
|
||||||
isFirstRecurringEvent: true,
|
isFirstRecurringEvent: true,
|
||||||
hideBranding: !!eventType.owner?.hideBranding,
|
hideBranding: !!eventType.owner?.hideBranding,
|
||||||
seatReferenceUid: evt.attendeeSeatId,
|
seatReferenceUid: evt.attendeeSeatId,
|
||||||
isKYCVerified,
|
eventTypeRequiresConfirmation: eventType.requiresConfirmation,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log.error("Error while scheduling workflow reminders", error);
|
log.error("Error while scheduling workflow reminders", error);
|
||||||
|
|
|
@ -39,7 +39,6 @@ interface IAddActionDialog {
|
||||||
senderId?: string,
|
senderId?: string,
|
||||||
senderName?: string
|
senderName?: string
|
||||||
) => void;
|
) => void;
|
||||||
setKYCVerificationDialogOpen: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ISelectActionOption {
|
interface ISelectActionOption {
|
||||||
|
@ -57,7 +56,7 @@ type AddActionFormValues = {
|
||||||
|
|
||||||
export const AddActionDialog = (props: IAddActionDialog) => {
|
export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { isOpenDialog, setIsOpenDialog, addAction, setKYCVerificationDialogOpen } = props;
|
const { isOpenDialog, setIsOpenDialog, addAction } = props;
|
||||||
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
|
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(false);
|
||||||
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(false);
|
const [isSenderIdNeeded, setIsSenderIdNeeded] = useState(false);
|
||||||
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);
|
const [isEmailAddressNeeded, setIsEmailAddressNeeded] = useState(false);
|
||||||
|
@ -171,14 +170,13 @@ export const AddActionDialog = (props: IAddActionDialog) => {
|
||||||
onChange={handleSelectAction}
|
onChange={handleSelectAction}
|
||||||
options={actionOptions.map((option) => ({
|
options={actionOptions.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
verificationAction: () => setKYCVerificationDialogOpen(),
|
|
||||||
}))}
|
}))}
|
||||||
isOptionDisabled={(option: {
|
isOptionDisabled={(option: {
|
||||||
label: string;
|
label: string;
|
||||||
value: WorkflowActions;
|
value: WorkflowActions;
|
||||||
needsUpgrade: boolean;
|
needsTeamsUpgrade: boolean;
|
||||||
needsVerification: boolean;
|
needsOrgsUpgrade: boolean;
|
||||||
}) => option.needsUpgrade || option.needsVerification}
|
}) => option.needsTeamsUpgrade || option.needsOrgsUpgrade}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { Trans } from "next-i18next";
|
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
|
||||||
|
|
||||||
import { SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { Dialog, DialogContent, DialogClose, DialogFooter } from "@calcom/ui";
|
|
||||||
|
|
||||||
export const KYCVerificationDialog = (props: {
|
|
||||||
isOpenDialog: boolean;
|
|
||||||
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
|
||||||
isPartOfTeam: boolean;
|
|
||||||
}) => {
|
|
||||||
const { isOpenDialog, setIsOpenDialog, isPartOfTeam } = props;
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
const isTeamString = isPartOfTeam ? "team" : "";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
|
||||||
<DialogContent title={t("verify_team_or_account", { teamOrAccount: isTeamString || "account" })}>
|
|
||||||
<div>
|
|
||||||
<div className="mb-4">
|
|
||||||
<Trans
|
|
||||||
i18nKey="kyc_verification_information"
|
|
||||||
components={{
|
|
||||||
a: (
|
|
||||||
<a
|
|
||||||
href={`mailto:support@cal.com?subject=${
|
|
||||||
isPartOfTeam ? "Team%20Verification" : "Account%20Verification"
|
|
||||||
}`}
|
|
||||||
style={{ color: "#3E3E3E" }}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
values={{
|
|
||||||
supportEmail:
|
|
||||||
SUPPORT_MAIL_ADDRESS === "help@cal.com" ? "support@cal.com" : SUPPORT_MAIL_ADDRESS,
|
|
||||||
teamOrAccount: isTeamString || "account",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Trans
|
|
||||||
i18nKey="kyc_verification_documents"
|
|
||||||
components={{ li: <li />, ul: <ul className="ml-8 list-disc" /> }}
|
|
||||||
values={{ teamOrUser: isPartOfTeam ? "team URL" : "user name" }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<DialogClose />
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -5,7 +5,6 @@ import type { UseFormReturn } from "react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
|
||||||
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
||||||
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { WorkflowTemplates } from "@calcom/prisma/enums";
|
import { WorkflowTemplates } from "@calcom/prisma/enums";
|
||||||
import type { WorkflowActions } from "@calcom/prisma/enums";
|
import type { WorkflowActions } from "@calcom/prisma/enums";
|
||||||
|
@ -19,7 +18,6 @@ import { isSMSAction, isWhatsappAction } from "../lib/actionHelperFunctions";
|
||||||
import type { FormValues } from "../pages/workflow";
|
import type { FormValues } from "../pages/workflow";
|
||||||
import { AddActionDialog } from "./AddActionDialog";
|
import { AddActionDialog } from "./AddActionDialog";
|
||||||
import { DeleteDialog } from "./DeleteDialog";
|
import { DeleteDialog } from "./DeleteDialog";
|
||||||
import { KYCVerificationDialog } from "./KYCVerificationDialog";
|
|
||||||
import WorkflowStepContainer from "./WorkflowStepContainer";
|
import WorkflowStepContainer from "./WorkflowStepContainer";
|
||||||
|
|
||||||
type User = RouterOutputs["viewer"]["me"];
|
type User = RouterOutputs["viewer"]["me"];
|
||||||
|
@ -41,15 +39,12 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);
|
const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);
|
||||||
const [isKYCVerificationDialogOpen, setKYCVerificationDialogOpen] = useState(false);
|
|
||||||
|
|
||||||
const [reload, setReload] = useState(false);
|
const [reload, setReload] = useState(false);
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.eventTypes.getByViewer.useQuery();
|
const { data, isLoading } = trpc.viewer.eventTypes.getByViewer.useQuery();
|
||||||
|
|
||||||
const isPartOfTeam = useHasTeamPlan();
|
|
||||||
|
|
||||||
const eventTypeOptions = useMemo(
|
const eventTypeOptions = useMemo(
|
||||||
() =>
|
() =>
|
||||||
data?.eventTypeGroups.reduce((options, group) => {
|
data?.eventTypeGroups.reduce((options, group) => {
|
||||||
|
@ -177,7 +172,6 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||||
user={props.user}
|
user={props.user}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
setKYCVerificationDialogOpen={setKYCVerificationDialogOpen}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -194,7 +188,6 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||||
setReload={setReload}
|
setReload={setReload}
|
||||||
teamId={teamId}
|
teamId={teamId}
|
||||||
readOnly={props.readOnly}
|
readOnly={props.readOnly}
|
||||||
setKYCVerificationDialogOpen={setKYCVerificationDialogOpen}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -222,12 +215,6 @@ export default function WorkflowDetailsPage(props: Props) {
|
||||||
isOpenDialog={isAddActionDialogOpen}
|
isOpenDialog={isAddActionDialogOpen}
|
||||||
setIsOpenDialog={setIsAddActionDialogOpen}
|
setIsOpenDialog={setIsAddActionDialogOpen}
|
||||||
addAction={addAction}
|
addAction={addAction}
|
||||||
setKYCVerificationDialogOpen={() => setKYCVerificationDialogOpen(true)}
|
|
||||||
/>
|
|
||||||
<KYCVerificationDialog
|
|
||||||
isOpenDialog={isKYCVerificationDialogOpen}
|
|
||||||
setIsOpenDialog={setKYCVerificationDialogOpen}
|
|
||||||
isPartOfTeam={!!isPartOfTeam.hasTeamPlan}
|
|
||||||
/>
|
/>
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
isOpenDialog={deleteDialogOpen}
|
isOpenDialog={deleteDialogOpen}
|
||||||
|
|
|
@ -67,7 +67,6 @@ type WorkflowStepProps = {
|
||||||
setReload?: Dispatch<SetStateAction<boolean>>;
|
setReload?: Dispatch<SetStateAction<boolean>>;
|
||||||
teamId?: number;
|
teamId?: number;
|
||||||
readOnly: boolean;
|
readOnly: boolean;
|
||||||
setKYCVerificationDialogOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
|
@ -331,15 +330,13 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (step && step.action) {
|
if (step && step.action) {
|
||||||
const templateValue = form.watch(`steps.${step.stepNumber - 1}.template`);
|
|
||||||
const actionString = t(`${step.action.toLowerCase()}_action`);
|
const actionString = t(`${step.action.toLowerCase()}_action`);
|
||||||
|
|
||||||
const selectedAction = {
|
const selectedAction = {
|
||||||
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
|
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
|
||||||
value: step.action,
|
value: step.action,
|
||||||
needsUpgrade: false,
|
needsTeamsUpgrade: false,
|
||||||
needsVerification: false,
|
needsOrgsUpgrade: false,
|
||||||
verificationAction: () => props.setKYCVerificationDialogOpen(true),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectedTemplate = { label: t(`${step.template.toLowerCase()}`), value: step.template };
|
const selectedTemplate = { label: t(`${step.template.toLowerCase()}`), value: step.template };
|
||||||
|
@ -530,14 +527,13 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
defaultValue={selectedAction}
|
defaultValue={selectedAction}
|
||||||
options={actionOptions?.map((option) => ({
|
options={actionOptions?.map((option) => ({
|
||||||
...option,
|
...option,
|
||||||
verificationAction: () => props.setKYCVerificationDialogOpen(true),
|
|
||||||
}))}
|
}))}
|
||||||
isOptionDisabled={(option: {
|
isOptionDisabled={(option: {
|
||||||
label: string;
|
label: string;
|
||||||
value: WorkflowActions;
|
value: WorkflowActions;
|
||||||
needsUpgrade: boolean;
|
needsTeamsUpgrade: boolean;
|
||||||
needsVerification: boolean;
|
needsOrgsUpgrade: boolean;
|
||||||
}) => option.needsUpgrade || option.needsVerification}
|
}) => option.needsTeamsUpgrade || option.needsOrgsUpgrade}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
@ -617,7 +613,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="-ml-[3px] h-[38px] min-w-fit sm:block sm:rounded-bl-none sm:rounded-tl-none "
|
className="-ml-[3px] h-[36px] min-w-fit py-0 sm:block sm:rounded-bl-none sm:rounded-tl-none "
|
||||||
disabled={verifyPhoneNumberMutation.isLoading || props.readOnly}
|
disabled={verifyPhoneNumberMutation.isLoading || props.readOnly}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
verifyPhoneNumberMutation.mutate({
|
verifyPhoneNumberMutation.mutate({
|
||||||
|
|
|
@ -41,6 +41,9 @@ export function isAttendeeAction(action: WorkflowActions) {
|
||||||
export function isTextMessageToAttendeeAction(action?: WorkflowActions) {
|
export function isTextMessageToAttendeeAction(action?: WorkflowActions) {
|
||||||
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
|
return action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.WHATSAPP_ATTENDEE;
|
||||||
}
|
}
|
||||||
|
export function isTextMessageToSpecificNumber(action?: WorkflowActions) {
|
||||||
|
return action === WorkflowActions.SMS_NUMBER || action === WorkflowActions.WHATSAPP_NUMBER;
|
||||||
|
}
|
||||||
|
|
||||||
export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): WorkflowTemplates {
|
export function getWhatsappTemplateForTrigger(trigger: WorkflowTriggerEvents): WorkflowTemplates {
|
||||||
switch (trigger) {
|
switch (trigger) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
WORKFLOW_TRIGGER_EVENTS,
|
WORKFLOW_TRIGGER_EVENTS,
|
||||||
} from "./constants";
|
} from "./constants";
|
||||||
|
|
||||||
export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean, isKYCVerified?: boolean) {
|
export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean, isOrgsPlan?: boolean) {
|
||||||
return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode
|
return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode
|
||||||
.map((action) => {
|
.map((action) => {
|
||||||
const actionString = t(`${action.toLowerCase()}_action`);
|
const actionString = t(`${action.toLowerCase()}_action`);
|
||||||
|
@ -23,8 +23,9 @@ export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean, is
|
||||||
return {
|
return {
|
||||||
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
|
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
|
||||||
value: action,
|
value: action,
|
||||||
needsUpgrade: isSMSOrWhatsappAction(action) && !isTeamsPlan,
|
needsTeamsUpgrade:
|
||||||
needsVerification: isTextMessageToAttendeeAction(action) && !isKYCVerified,
|
isSMSOrWhatsappAction(action) && !isTextMessageToAttendeeAction(action) && !isTeamsPlan,
|
||||||
|
needsOrgsUpgrade: isTextMessageToAttendeeAction(action) && !isOrgsPlan,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
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;
|
|
||||||
}
|
|
|
@ -23,7 +23,6 @@ type ProcessWorkflowStepParams = {
|
||||||
emailAttendeeSendToOverride?: string;
|
emailAttendeeSendToOverride?: string;
|
||||||
hideBranding?: boolean;
|
hideBranding?: boolean;
|
||||||
seatReferenceUid?: string;
|
seatReferenceUid?: string;
|
||||||
isKYCVerified: boolean;
|
|
||||||
eventTypeRequiresConfirmation?: boolean;
|
eventTypeRequiresConfirmation?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,11 +47,9 @@ const processWorkflowStep = async (
|
||||||
hideBranding,
|
hideBranding,
|
||||||
seatReferenceUid,
|
seatReferenceUid,
|
||||||
eventTypeRequiresConfirmation,
|
eventTypeRequiresConfirmation,
|
||||||
isKYCVerified,
|
|
||||||
}: ProcessWorkflowStepParams
|
}: ProcessWorkflowStepParams
|
||||||
) => {
|
) => {
|
||||||
if (isTextMessageToAttendeeAction(step.action) && (!isKYCVerified || !eventTypeRequiresConfirmation))
|
if (isTextMessageToAttendeeAction(step.action) && !eventTypeRequiresConfirmation) return;
|
||||||
return;
|
|
||||||
|
|
||||||
if (step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER) {
|
if (step.action === WorkflowActions.SMS_ATTENDEE || step.action === WorkflowActions.SMS_NUMBER) {
|
||||||
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
|
const sendTo = step.action === WorkflowActions.SMS_ATTENDEE ? smsReminderNumber : step.sendTo;
|
||||||
|
@ -143,7 +140,6 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
|
||||||
hideBranding,
|
hideBranding,
|
||||||
seatReferenceUid,
|
seatReferenceUid,
|
||||||
eventTypeRequiresConfirmation = false,
|
eventTypeRequiresConfirmation = false,
|
||||||
isKYCVerified,
|
|
||||||
} = args;
|
} = args;
|
||||||
if (isNotConfirmed || !workflows.length) return;
|
if (isNotConfirmed || !workflows.length) return;
|
||||||
|
|
||||||
|
@ -177,7 +173,6 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA
|
||||||
hideBranding,
|
hideBranding,
|
||||||
seatReferenceUid,
|
seatReferenceUid,
|
||||||
eventTypeRequiresConfirmation,
|
eventTypeRequiresConfirmation,
|
||||||
isKYCVerified,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -208,13 +203,11 @@ export interface SendCancelledRemindersArgs {
|
||||||
smsReminderNumber: string | null;
|
smsReminderNumber: string | null;
|
||||||
evt: ExtendedCalendarEvent;
|
evt: ExtendedCalendarEvent;
|
||||||
hideBranding?: boolean;
|
hideBranding?: boolean;
|
||||||
isKYCVerified: boolean;
|
|
||||||
eventTypeRequiresConfirmation?: boolean;
|
eventTypeRequiresConfirmation?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
|
export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) => {
|
||||||
const { workflows, smsReminderNumber, evt, hideBranding, isKYCVerified, eventTypeRequiresConfirmation } =
|
const { workflows, smsReminderNumber, evt, hideBranding, eventTypeRequiresConfirmation } = args;
|
||||||
args;
|
|
||||||
if (!workflows.length) return;
|
if (!workflows.length) return;
|
||||||
|
|
||||||
for (const workflowRef of workflows) {
|
for (const workflowRef of workflows) {
|
||||||
|
@ -228,7 +221,6 @@ export const sendCancelledReminders = async (args: SendCancelledRemindersArgs) =
|
||||||
hideBranding,
|
hideBranding,
|
||||||
calendarEvent: evt,
|
calendarEvent: evt,
|
||||||
eventTypeRequiresConfirmation,
|
eventTypeRequiresConfirmation,
|
||||||
isKYCVerified,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -117,7 +117,6 @@ const tabs: VerticalTabItemProps[] = [
|
||||||
{ name: "apps", href: "/settings/admin/apps/calendar" },
|
{ name: "apps", href: "/settings/admin/apps/calendar" },
|
||||||
{ name: "users", href: "/settings/admin/users" },
|
{ name: "users", href: "/settings/admin/users" },
|
||||||
{ name: "organizations", href: "/settings/admin/organizations" },
|
{ name: "organizations", href: "/settings/admin/organizations" },
|
||||||
{ name: "kyc_verification", href: "/settings/admin/kycVerification" },
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -41,7 +41,6 @@ const ENDPOINTS = [
|
||||||
"workflows",
|
"workflows",
|
||||||
"appsRouter",
|
"appsRouter",
|
||||||
"googleWorkspace",
|
"googleWorkspace",
|
||||||
"kycVerification",
|
|
||||||
] as const;
|
] as const;
|
||||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import { bookingsRouter } from "./bookings/_router";
|
||||||
import { deploymentSetupRouter } from "./deploymentSetup/_router";
|
import { deploymentSetupRouter } from "./deploymentSetup/_router";
|
||||||
import { eventTypesRouter } from "./eventTypes/_router";
|
import { eventTypesRouter } from "./eventTypes/_router";
|
||||||
import { googleWorkspaceRouter } from "./googleWorkspace/_router";
|
import { googleWorkspaceRouter } from "./googleWorkspace/_router";
|
||||||
import { kycVerificationRouter } from "./kycVerification/_router";
|
|
||||||
import { viewerOrganizationsRouter } from "./organizations/_router";
|
import { viewerOrganizationsRouter } from "./organizations/_router";
|
||||||
import { paymentsRouter } from "./payments/_router";
|
import { paymentsRouter } from "./payments/_router";
|
||||||
import { slotsRouter } from "./slots/_router";
|
import { slotsRouter } from "./slots/_router";
|
||||||
|
@ -53,6 +52,5 @@ export const viewerRouter = mergeRouters(
|
||||||
users: userAdminRouter,
|
users: userAdminRouter,
|
||||||
googleWorkspace: googleWorkspaceRouter,
|
googleWorkspace: googleWorkspaceRouter,
|
||||||
admin: adminRouter,
|
admin: adminRouter,
|
||||||
kycVerification: kycVerificationRouter,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
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,
|
|
||||||
});
|
|
||||||
}),
|
|
||||||
});
|
|
|
@ -1,44 +0,0 @@
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
|
||||||
import { prisma } from "@calcom/prisma";
|
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
|
||||||
|
|
||||||
type IsKYCVerifiedOptions = {
|
|
||||||
ctx: {
|
|
||||||
user: NonNullable<TrpcSessionUser>;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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 };
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
export {};
|
|
|
@ -1,76 +0,0 @@
|
||||||
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<TrpcSessionUser>;
|
|
||||||
};
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
|
@ -1,8 +0,0 @@
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export const ZVerifyInputSchema = z.object({
|
|
||||||
name: z.string(),
|
|
||||||
isTeam: z.boolean(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type TVerifyInputSchema = z.infer<typeof ZVerifyInputSchema>;
|
|
|
@ -4,7 +4,6 @@ import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { isVerifiedHandler } from "../kycVerification/isVerified.handler";
|
|
||||||
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
||||||
|
|
||||||
type GetWorkflowActionOptionsOptions = {
|
type GetWorkflowActionOptionsOptions = {
|
||||||
|
@ -27,12 +26,12 @@ export const getWorkflowActionOptionsHandler = async ({ ctx }: GetWorkflowAction
|
||||||
isTeamsPlan = !!hasTeamPlan;
|
isTeamsPlan = !!hasTeamPlan;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isKYCVerified } = await isVerifiedHandler({ ctx });
|
const hasOrgsPlan = !!user.organizationId;
|
||||||
|
|
||||||
const t = await getTranslation(ctx.user.locale, "common");
|
const t = await getTranslation(ctx.user.locale, "common");
|
||||||
return getWorkflowActionOptions(
|
return getWorkflowActionOptions(
|
||||||
t,
|
t,
|
||||||
IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan,
|
IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan,
|
||||||
isKYCVerified
|
IS_SELF_HOSTED || hasOrgsPlan
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
import { sendVerificationCode } from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber";
|
import { sendVerificationCode } from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber";
|
||||||
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
|
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
||||||
import type { TSendVerificationCodeInputSchema } from "./sendVerificationCode.schema";
|
import type { TSendVerificationCodeInputSchema } from "./sendVerificationCode.schema";
|
||||||
|
|
||||||
type SendVerificationCodeOptions = {
|
type SendVerificationCodeOptions = {
|
||||||
|
@ -10,7 +14,22 @@ type SendVerificationCodeOptions = {
|
||||||
input: TSendVerificationCodeInputSchema;
|
input: TSendVerificationCodeInputSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const sendVerificationCodeHandler = async ({ ctx: _ctx, input }: SendVerificationCodeOptions) => {
|
export const sendVerificationCodeHandler = async ({ ctx, input }: SendVerificationCodeOptions) => {
|
||||||
|
const { user } = ctx;
|
||||||
|
|
||||||
|
const isCurrentUsernamePremium =
|
||||||
|
user && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
|
||||||
|
|
||||||
|
let isTeamsPlan = false;
|
||||||
|
if (!isCurrentUsernamePremium) {
|
||||||
|
const { hasTeamPlan } = await hasTeamPlanHandler({ ctx });
|
||||||
|
isTeamsPlan = !!hasTeamPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCurrentUsernamePremium && !isTeamsPlan) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
const { phoneNumber } = input;
|
const { phoneNumber } = input;
|
||||||
return sendVerificationCode(phoneNumber);
|
return sendVerificationCode(phoneNumber);
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,6 +3,7 @@ import type { Prisma } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
isSMSOrWhatsappAction,
|
isSMSOrWhatsappAction,
|
||||||
isTextMessageToAttendeeAction,
|
isTextMessageToAttendeeAction,
|
||||||
|
isTextMessageToSpecificNumber,
|
||||||
} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
|
} from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
|
||||||
import {
|
import {
|
||||||
deleteScheduledEmailReminder,
|
deleteScheduledEmailReminder,
|
||||||
|
@ -25,7 +26,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||||
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
|
|
||||||
import { isVerifiedHandler } from "../kycVerification/isVerified.handler";
|
|
||||||
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
||||||
import type { TUpdateInputSchema } from "./update.schema";
|
import type { TUpdateInputSchema } from "./update.schema";
|
||||||
import {
|
import {
|
||||||
|
@ -84,8 +84,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
}
|
}
|
||||||
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;
|
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;
|
||||||
|
|
||||||
const kycVerified = await isVerifiedHandler({ ctx });
|
const hasOrgsPlan = IS_SELF_HOSTED || ctx.user.organizationId;
|
||||||
const isKYCVerified = kycVerified.isKYCVerified;
|
|
||||||
|
|
||||||
const activeOnEventTypes = await ctx.prisma.eventType.findMany({
|
const activeOnEventTypes = await ctx.prisma.eventType.findMany({
|
||||||
where: {
|
where: {
|
||||||
|
@ -425,11 +424,21 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
|
|
||||||
//step was edited
|
//step was edited
|
||||||
} else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) {
|
} else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) {
|
||||||
if (!hasPaidPlan && !isSMSOrWhatsappAction(oldStep.action) && isSMSOrWhatsappAction(newStep.action)) {
|
// check if step that require team plan already existed before
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
if (
|
||||||
|
!hasPaidPlan &&
|
||||||
|
!isTextMessageToSpecificNumber(oldStep.action) &&
|
||||||
|
isTextMessageToSpecificNumber(newStep.action)
|
||||||
|
) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
|
||||||
}
|
}
|
||||||
if (!isKYCVerified && isTextMessageToAttendeeAction(newStep.action)) {
|
// check if step that require org already existed before
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Account needs to be verified" });
|
if (
|
||||||
|
!hasOrgsPlan &&
|
||||||
|
!isTextMessageToAttendeeAction(oldStep.action) &&
|
||||||
|
isTextMessageToAttendeeAction(newStep.action)
|
||||||
|
) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Enterprise plan required" });
|
||||||
}
|
}
|
||||||
const requiresSender =
|
const requiresSender =
|
||||||
newStep.action === WorkflowActions.SMS_NUMBER || newStep.action === WorkflowActions.WHATSAPP_NUMBER;
|
newStep.action === WorkflowActions.SMS_NUMBER || newStep.action === WorkflowActions.WHATSAPP_NUMBER;
|
||||||
|
@ -605,11 +614,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
||||||
//added steps
|
//added steps
|
||||||
const addedSteps = steps.map((s) => {
|
const addedSteps = steps.map((s) => {
|
||||||
if (s.id <= 0) {
|
if (s.id <= 0) {
|
||||||
if (isSMSOrWhatsappAction(s.action) && !hasPaidPlan) {
|
if (isSMSOrWhatsappAction(s.action) && !isTextMessageToAttendeeAction(s.action) && !hasPaidPlan) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Not available on free plan" });
|
||||||
}
|
}
|
||||||
if (!isKYCVerified && isTextMessageToAttendeeAction(s.action)) {
|
if (!hasOrgsPlan && isTextMessageToAttendeeAction(s.action)) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Account needs to be verified" });
|
throw new TRPCError({ code: "UNAUTHORIZED", message: "Enterprise plan require" });
|
||||||
}
|
}
|
||||||
const { id: _stepId, ...stepToAdd } = s;
|
const { id: _stepId, ...stepToAdd } = s;
|
||||||
return stepToAdd;
|
return stepToAdd;
|
||||||
|
|
|
@ -1,20 +0,0 @@
|
||||||
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 (
|
|
||||||
<>
|
|
||||||
<Tooltip content={t("verify_team_tooltip")}>
|
|
||||||
<Badge variant="gray" onClick={() => verifyTeamAction()}>
|
|
||||||
{t("verify_account")}
|
|
||||||
</Badge>
|
|
||||||
</Tooltip>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
import { Tooltip } from "../tooltip";
|
||||||
|
import { Badge } from "./Badge";
|
||||||
|
|
||||||
|
export const UpgradeOrgsBadge = function UpgradeOrgsBadge() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip content={t("orgs_upgrade_to_enable_feature")}>
|
||||||
|
<a href="https://cal.com/enterprise" target="_blank">
|
||||||
|
<Badge variant="gray">{t("upgrade")}</Badge>
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
export { Badge } from "./Badge";
|
export { Badge } from "./Badge";
|
||||||
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
|
export { UpgradeTeamsBadge } from "./UpgradeTeamsBadge";
|
||||||
export { KYCVerificationBadge } from "./KYCVerificationBadge";
|
export { UpgradeOrgsBadge } from "./UpgradeOrgsBadge";
|
||||||
|
|
||||||
export type { BadgeProps } from "./Badge";
|
export type { BadgeProps } from "./Badge";
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { components as reactSelectComponents } from "react-select";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
|
||||||
import { UpgradeTeamsBadge, KYCVerificationBadge } from "../../badge";
|
import { UpgradeTeamsBadge, UpgradeOrgsBadge } from "../../badge";
|
||||||
import { Check } from "../../icon";
|
import { Check } from "../../icon";
|
||||||
|
|
||||||
export const InputComponent = <
|
export const InputComponent = <
|
||||||
|
@ -29,9 +29,8 @@ export const InputComponent = <
|
||||||
type ExtendedOption = {
|
type ExtendedOption = {
|
||||||
value: string | number;
|
value: string | number;
|
||||||
label: string;
|
label: string;
|
||||||
needsUpgrade?: boolean;
|
needsTeamsUpgrade?: boolean;
|
||||||
needsVerification?: boolean;
|
needsOrgsUpgrade?: boolean;
|
||||||
verificationAction?: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OptionComponent = <
|
export const OptionComponent = <
|
||||||
|
@ -48,12 +47,10 @@ export const OptionComponent = <
|
||||||
<span className="mr-auto" data-testid={`select-option-${(props as unknown as ExtendedOption).value}`}>
|
<span className="mr-auto" data-testid={`select-option-${(props as unknown as ExtendedOption).value}`}>
|
||||||
{props.label || <> </>}
|
{props.label || <> </>}
|
||||||
</span>
|
</span>
|
||||||
{(props.data as unknown as ExtendedOption).needsUpgrade ? (
|
{(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? (
|
||||||
<UpgradeTeamsBadge />
|
<UpgradeTeamsBadge />
|
||||||
) : (props.data as unknown as ExtendedOption).needsVerification ? (
|
) : (props.data as unknown as ExtendedOption).needsOrgsUpgrade ? (
|
||||||
<KYCVerificationBadge
|
<UpgradeOrgsBadge />
|
||||||
verifyTeamAction={(props.data as unknown as ExtendedOption).verificationAction}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue