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
Carina Wollendorfer 2023-09-21 08:22:05 +02:00 committed by GitHub
parent 96263b0cf7
commit 3b50fe075d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 89 additions and 542 deletions

View File

@ -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);

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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.",

View File

@ -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,
}); });
} }

View File

@ -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,
}); });
} }
} }

View File

@ -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);

View File

@ -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}
/> />
); );
}} }}

View File

@ -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>
);
};

View File

@ -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}

View File

@ -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({

View File

@ -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) {

View File

@ -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,
}; };
}); });
} }

View File

@ -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;
}

View File

@ -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,
}); });
} }
} }

View File

@ -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" },
], ],
}, },
]; ];

View File

@ -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];

View File

@ -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,
}) })
); );

View File

@ -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,
});
}),
});

View File

@ -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 };
};

View File

@ -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,
};
};

View File

@ -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>;

View File

@ -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
); );
}; };

View File

@ -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);
}; };

View File

@ -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;

View File

@ -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>
</>
);
};

View File

@ -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>
);
};

View File

@ -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";

View File

@ -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 || <>&nbsp;</>} {props.label || <>&nbsp;</>}
</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}
/>
) : ( ) : (
<></> <></>
)} )}