Feat/verify phone number (#6035)
* first implementation of verifying phone number * add UI + logic for verifying phone number flow * check if all phone numbers are verified before submit * add numberVerification pending * only send SMS to verified numbers * fix design for mobile view * check if phone number is verified before testing action * add back message service id check * add TWILIO_VERIFY_SID to .env.example * code clean up * add TWILIO_VERIFY_SID to README.md instructions * save new verified numbers * fix wrongly thrown error for new verified numbers * use false as default value for isVerificationPending paramater * add translations * add migration file * code clean up * don't throw error if phone number is not needed * Feedback * Shows error when Twillio isn't properly setup * Type fixes Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
parent
688541923b
commit
c2f150dbab
|
@ -87,6 +87,7 @@ TWILIO_TOKEN=
|
|||
TWILIO_MESSAGING_SID=
|
||||
TWILIO_PHONE_NUMBER=
|
||||
NEXT_PUBLIC_SENDER_ID=
|
||||
TWILIO_VERIFY_SID=
|
||||
|
||||
# This is used so we can bypass emails in auth flows for E2E testing
|
||||
# Set it to "1" if you need to run E2E tests locally
|
||||
|
|
|
@ -429,6 +429,8 @@ following
|
|||
12. Leave all other fields as they are
|
||||
13. Complete setup and click ‘View my new Messaging Service’
|
||||
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
|
||||
15. Create a verify service
|
||||
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
|
||||
|
||||
<!-- LICENSE -->
|
||||
|
||||
|
|
|
@ -1455,6 +1455,12 @@
|
|||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||
"set_as_default": "Set as default",
|
||||
"hide_eventtype_details": "Hide EventType Details",
|
||||
"verification_code_sent": "Verification code sent",
|
||||
"verified_successfully": "Verified successfully",
|
||||
"wrong_code": "Wong verification code",
|
||||
"not_verified": "Not yet verified",
|
||||
"no_availability_in_month": "No availability in {{month}}",
|
||||
"view_next_month": "View next month"
|
||||
"view_next_month": "View next month",
|
||||
"send_code" : "Send code",
|
||||
"number_verified": "Number Verified"
|
||||
}
|
||||
|
|
|
@ -6,10 +6,8 @@ import { Controller, UseFormReturn } from "react-hook-form";
|
|||
import { SENDER_ID } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Button, Label, TextField } from "@calcom/ui";
|
||||
import { MultiSelectCheckboxes } from "@calcom/ui";
|
||||
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
|
||||
import { Button, Icon, Label, MultiSelectCheckboxes, TextField } from "@calcom/ui";
|
||||
|
||||
import type { FormValues } from "../pages/workflow";
|
||||
import { AddActionDialog } from "./AddActionDialog";
|
||||
|
@ -74,6 +72,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
|||
template: WorkflowTemplates.CUSTOM,
|
||||
numberRequired: numberRequired || false,
|
||||
sender: sender || SENDER_ID,
|
||||
numberVerificationPending: false,
|
||||
};
|
||||
steps?.push(step);
|
||||
form.setValue("steps", steps);
|
||||
|
|
|
@ -9,11 +9,13 @@ import { Dispatch, SetStateAction, useRef, useState } from "react";
|
|||
import { Controller, UseFormReturn } from "react-hook-form";
|
||||
import "react-phone-number-input/style.css";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { SENDER_ID } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
ConfirmationDialogContent,
|
||||
|
@ -35,11 +37,7 @@ import {
|
|||
} from "@calcom/ui";
|
||||
|
||||
import { AddVariablesDropdown } from "../components/AddVariablesDropdown";
|
||||
import {
|
||||
getWorkflowActionOptions,
|
||||
getWorkflowTemplateOptions,
|
||||
getWorkflowTriggerOptions,
|
||||
} from "../lib/getOptions";
|
||||
import { getWorkflowTemplateOptions, getWorkflowTriggerOptions } from "../lib/getOptions";
|
||||
import { translateVariablesToEnglish } from "../lib/variableTranslations";
|
||||
import type { FormValues } from "../pages/workflow";
|
||||
import Editor from "./TextEditor/Editor";
|
||||
|
@ -54,10 +52,15 @@ type WorkflowStepProps = {
|
|||
|
||||
export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
||||
const { t, i18n } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { step, form, reload, setReload } = props;
|
||||
const { data: _verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery();
|
||||
const verifiedNumbers = _verifiedNumbers?.map((number) => number.phoneNumber);
|
||||
const [isAdditionalInputsDialogOpen, setIsAdditionalInputsDialogOpen] = useState(false);
|
||||
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
|
||||
|
||||
const [verificationCode, setVerificationCode] = useState("");
|
||||
|
||||
const [isPhoneNumberNeeded, setIsPhoneNumberNeeded] = useState(
|
||||
step?.action === WorkflowActions.SMS_NUMBER ? true : false
|
||||
);
|
||||
|
@ -108,6 +111,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
|
||||
const refReminderBody = useRef<HTMLTextAreaElement | null>(null);
|
||||
|
||||
const [numberVerified, setNumberVerified] = useState(
|
||||
verifiedNumbers && step
|
||||
? !!verifiedNumbers.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`))
|
||||
: false
|
||||
);
|
||||
|
||||
const addVariable = (variable: string, isEmailSubject?: boolean) => {
|
||||
if (step) {
|
||||
if (isEmailSubject) {
|
||||
|
@ -128,6 +137,30 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const sendVerificationCodeMutation = trpc.viewer.workflows.sendVerificationCode.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(t("verification_code_sent"), "success");
|
||||
},
|
||||
onError: async (error) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const verifyPhoneNumberMutation = trpc.viewer.workflows.verifyPhoneNumber.useMutation({
|
||||
onSuccess: async (isVerified) => {
|
||||
showToast(isVerified ? t("verified_successfully") : t("wrong_code"), "success");
|
||||
setNumberVerified(isVerified);
|
||||
utils.viewer.workflows.getVerifiedNumbers.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setNumberVerified(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const testActionMutation = trpc.viewer.workflows.testAction.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(t("notification_sent"), "success");
|
||||
|
@ -137,6 +170,8 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
if (err instanceof TRPCClientError) {
|
||||
if (err.message === "rate-limit-exceeded") {
|
||||
message = t("rate_limit_exceeded");
|
||||
} else {
|
||||
message = err.message;
|
||||
}
|
||||
}
|
||||
if (err instanceof HttpError) {
|
||||
|
@ -311,7 +346,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
setIsSenderIdNeeded(true);
|
||||
setIsEmailAddressNeeded(false);
|
||||
setIsPhoneNumberNeeded(val.value === WorkflowActions.SMS_NUMBER);
|
||||
|
||||
setNumberVerified(false);
|
||||
if (!wasSMSAction) {
|
||||
form.setValue(`steps.${step.stepNumber - 1}.reminderBody`, "");
|
||||
}
|
||||
|
@ -356,20 +391,76 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
{isPhoneNumberNeeded && (
|
||||
<>
|
||||
<Label className="pt-4">{t("custom_phone_number")}</Label>
|
||||
<div className="block sm:flex">
|
||||
<PhoneInput<FormValues>
|
||||
control={form.control}
|
||||
name={`steps.${step.stepNumber - 1}.sendTo`}
|
||||
placeholder={t("phone_number")}
|
||||
id={`steps.${step.stepNumber - 1}.sendTo`}
|
||||
className="w-full rounded-md"
|
||||
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
|
||||
required
|
||||
onChange={() => {
|
||||
const isAlreadyVerified = !!verifiedNumbers
|
||||
?.concat([])
|
||||
.find(
|
||||
(number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
|
||||
);
|
||||
setNumberVerified(isAlreadyVerified);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
disabled={numberVerified || false}
|
||||
className={classNames(
|
||||
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
|
||||
numberVerified ? "hidden" : "mt-3 sm:mt-0"
|
||||
)}
|
||||
onClick={() =>
|
||||
sendVerificationCodeMutation.mutate({
|
||||
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
|
||||
})
|
||||
}>
|
||||
{t("send_code")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{form.formState.errors.steps &&
|
||||
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
|
||||
<p className="mt-1 text-xs text-red-500">
|
||||
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
|
||||
</p>
|
||||
)}
|
||||
{numberVerified ? (
|
||||
<div className="mt-1">
|
||||
<Badge variant="green">{t("number_verified")}</Badge>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="mt-3 flex">
|
||||
<TextField
|
||||
className=" border-r-transparent"
|
||||
placeholder="Verification code"
|
||||
value={verificationCode}
|
||||
onChange={(e) => {
|
||||
setVerificationCode(e.target.value);
|
||||
}}
|
||||
required
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="-ml-[3px] rounded-tl-none rounded-bl-none "
|
||||
disabled={verifyPhoneNumberMutation.isLoading}
|
||||
onClick={() => {
|
||||
verifyPhoneNumberMutation.mutate({
|
||||
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
|
||||
code: verificationCode,
|
||||
});
|
||||
}}>
|
||||
Verify
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isSenderIdNeeded && (
|
||||
|
@ -525,6 +616,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
className="mt-7 w-full"
|
||||
onClick={() => {
|
||||
let isEmpty = false;
|
||||
|
||||
if (!form.getValues(`steps.${step.stepNumber - 1}.sendTo`) && isPhoneNumberNeeded) {
|
||||
form.setError(`steps.${step.stepNumber - 1}.sendTo`, {
|
||||
type: "custom",
|
||||
|
@ -532,6 +624,13 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
});
|
||||
isEmpty = true;
|
||||
}
|
||||
|
||||
if (!numberVerified && isPhoneNumberNeeded) {
|
||||
form.setError(`steps.${step.stepNumber - 1}.sendTo`, {
|
||||
type: "custom",
|
||||
message: t("not_verified"),
|
||||
});
|
||||
}
|
||||
if (
|
||||
form.getValues(`steps.${step.stepNumber - 1}.template`) === WorkflowTemplates.CUSTOM
|
||||
) {
|
||||
|
@ -576,7 +675,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
|
|||
? false
|
||||
: true;
|
||||
|
||||
if (isPhoneNumberNeeded && isNumberValid && !isEmpty) {
|
||||
if (isPhoneNumberNeeded && isNumberValid && !isEmpty && numberVerified) {
|
||||
setConfirmationDialogOpen(true);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,7 +52,9 @@ export const scheduleWorkflowReminders = async (
|
|||
step.reminderBody || "",
|
||||
step.id,
|
||||
step.template,
|
||||
step.sender || SENDER_ID
|
||||
step.sender || SENDER_ID,
|
||||
workflow.userId,
|
||||
step.numberVerificationPending
|
||||
);
|
||||
} else if (
|
||||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||
|
@ -121,7 +123,9 @@ export const sendCancelledReminders = async (
|
|||
step.reminderBody || "",
|
||||
step.id,
|
||||
step.template,
|
||||
step.sender || SENDER_ID
|
||||
step.sender || SENDER_ID,
|
||||
workflow.userId,
|
||||
step.numberVerificationPending
|
||||
);
|
||||
} else if (
|
||||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||
|
|
|
@ -49,3 +49,26 @@ export const cancelSMS = async (referenceId: string) => {
|
|||
assertTwilio(twilio);
|
||||
await twilio.messages(referenceId).update({ status: "canceled" });
|
||||
};
|
||||
|
||||
export const sendVerificationCode = async (phoneNumber: string) => {
|
||||
assertTwilio(twilio);
|
||||
if (process.env.TWILIO_VERIFY_SID) {
|
||||
await twilio.verify
|
||||
.services(process.env.TWILIO_VERIFY_SID)
|
||||
.verifications.create({ to: phoneNumber, channel: "sms" });
|
||||
}
|
||||
};
|
||||
|
||||
export const verifyNumber = async (phoneNumber: string, code: string) => {
|
||||
assertTwilio(twilio);
|
||||
if (process.env.TWILIO_VERIFY_SID) {
|
||||
try {
|
||||
const verification_check = await twilio.verify.v2
|
||||
.services(process.env.TWILIO_VERIFY_SID)
|
||||
.verificationChecks.create({ to: phoneNumber, code: code });
|
||||
return verification_check.status;
|
||||
} catch (e) {
|
||||
return "failed";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -50,7 +50,9 @@ export const scheduleSMSReminder = async (
|
|||
message: string,
|
||||
workflowStepId: number,
|
||||
template: WorkflowTemplates,
|
||||
sender: string
|
||||
sender: string,
|
||||
userId: number,
|
||||
isVerificationPending = false
|
||||
) => {
|
||||
const { startTime, endTime } = evt;
|
||||
const uid = evt.uid as string;
|
||||
|
@ -60,6 +62,18 @@ export const scheduleSMSReminder = async (
|
|||
|
||||
const senderID = getSenderId(reminderPhone, sender);
|
||||
|
||||
//SMS_ATTENDEE action does not need to be verified
|
||||
//isVerificationPending is from all already existing workflows (once they edit their workflow, they will also have to verify the number)
|
||||
async function getIsNumberVerified() {
|
||||
if (action === WorkflowActions.SMS_ATTENDEE) return true;
|
||||
const verifiedNumber = await prisma.verifiedNumber.findFirst({
|
||||
where: { userId, phoneNumber: reminderPhone || "" },
|
||||
});
|
||||
if (!!verifiedNumber) return true;
|
||||
return isVerificationPending;
|
||||
}
|
||||
const isNumberVerified = await getIsNumberVerified();
|
||||
|
||||
if (triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT) {
|
||||
scheduledDate = timeSpan.time && timeUnit ? dayjs(startTime).subtract(timeSpan.time, timeUnit) : null;
|
||||
} else if (triggerEvent === WorkflowTriggerEvents.AFTER_EVENT) {
|
||||
|
@ -93,7 +107,7 @@ export const scheduleSMSReminder = async (
|
|||
break;
|
||||
}
|
||||
|
||||
if (message.length > 0 && reminderPhone) {
|
||||
if (message.length > 0 && reminderPhone && isNumberVerified) {
|
||||
//send SMS when event is booked/cancelled/rescheduled
|
||||
if (
|
||||
triggerEvent === WorkflowTriggerEvents.NEW_EVENT ||
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import prisma from "@calcom/prisma";
|
||||
|
||||
import * as twilio from "./smsProviders/twilioProvider";
|
||||
|
||||
export const sendVerificationCode = async (phoneNumber: string) => {
|
||||
return twilio.sendVerificationCode(phoneNumber);
|
||||
};
|
||||
|
||||
export const verifyPhoneNumber = async (phoneNumber: string, code: string, userId: number) => {
|
||||
const verificationStatus = await twilio.verifyNumber(phoneNumber, code);
|
||||
|
||||
if (verificationStatus === "approved") {
|
||||
await prisma.verifiedNumber.create({
|
||||
data: {
|
||||
userId,
|
||||
phoneNumber,
|
||||
},
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
|
@ -104,6 +104,8 @@ function WorkflowPage() {
|
|||
}
|
||||
);
|
||||
|
||||
const { data: verifiedNumbers } = trpc.viewer.workflows.getVerifiedNumbers.useQuery();
|
||||
|
||||
useEffect(() => {
|
||||
if (workflow && !isLoading) {
|
||||
setSelectedEventTypes(
|
||||
|
@ -175,6 +177,7 @@ function WorkflowPage() {
|
|||
handleSubmit={async (values) => {
|
||||
let activeOnEventTypeIds: number[] = [];
|
||||
let isEmpty = false;
|
||||
let isVerified = true;
|
||||
|
||||
values.steps.forEach((step) => {
|
||||
const isSMSAction =
|
||||
|
@ -199,9 +202,22 @@ function WorkflowPage() {
|
|||
step.emailSubject = translateVariablesToEnglish(step.emailSubject, { locale: i18n.language, t });
|
||||
}
|
||||
isEmpty = !isEmpty ? isBodyEmpty : isEmpty;
|
||||
|
||||
//check if phone number is verified
|
||||
if (
|
||||
step.action === WorkflowActions.SMS_NUMBER &&
|
||||
!verifiedNumbers?.find((verifiedNumber) => verifiedNumber.phoneNumber === step.sendTo)
|
||||
) {
|
||||
isVerified = false;
|
||||
|
||||
form.setError(`steps.${step.stepNumber - 1}.sendTo`, {
|
||||
type: "custom",
|
||||
message: t("not_verified"),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!isEmpty) {
|
||||
if (!isEmpty && isVerified) {
|
||||
if (values.activeOn) {
|
||||
activeOnEventTypeIds = values.activeOn.map((option) => {
|
||||
return parseInt(option.value, 10);
|
||||
|
@ -216,6 +232,7 @@ function WorkflowPage() {
|
|||
time: values.time || null,
|
||||
timeUnit: values.timeUnit || null,
|
||||
});
|
||||
utils.viewer.workflows.getVerifiedNumbers.invalidate();
|
||||
}
|
||||
}}>
|
||||
<Shell
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "WorkflowStep" ADD COLUMN "numberVerificationPending" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "VerifiedNumber" (
|
||||
"id" SERIAL NOT NULL,
|
||||
"userId" INTEGER NOT NULL,
|
||||
"phoneNumber" TEXT NOT NULL,
|
||||
|
||||
CONSTRAINT "VerifiedNumber_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "VerifiedNumber" ADD CONSTRAINT "VerifiedNumber_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
@ -186,6 +186,7 @@ model User {
|
|||
ownedEventTypes EventType[] @relation("owner")
|
||||
workflows Workflow[]
|
||||
routingForms App_RoutingForms_Form[] @relation("routing-form")
|
||||
verifiedNumbers VerifiedNumber[]
|
||||
|
||||
@@map(name: "users")
|
||||
}
|
||||
|
@ -573,6 +574,7 @@ model WorkflowStep {
|
|||
workflowReminders WorkflowReminder[]
|
||||
numberRequired Boolean?
|
||||
sender String?
|
||||
numberVerificationPending Boolean @default(true)
|
||||
}
|
||||
|
||||
model Workflow {
|
||||
|
@ -622,3 +624,10 @@ enum WorkflowMethods {
|
|||
EMAIL
|
||||
SMS
|
||||
}
|
||||
|
||||
model VerifiedNumber {
|
||||
id Int @id @default(autoincrement())
|
||||
userId Int
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
phoneNumber String
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ import {
|
|||
deleteScheduledSMSReminder,
|
||||
scheduleSMSReminder,
|
||||
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
||||
import {
|
||||
verifyPhoneNumber,
|
||||
sendVerificationCode,
|
||||
} from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber";
|
||||
import { SENDER_ID } from "@calcom/lib/constants";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
|
@ -162,6 +166,7 @@ export const workflowsRouter = router({
|
|||
template: WorkflowTemplates.REMINDER,
|
||||
workflowId: workflow.id,
|
||||
sender: SENDER_ID,
|
||||
numberVerificationPending: false,
|
||||
},
|
||||
});
|
||||
return { workflow };
|
||||
|
@ -481,7 +486,8 @@ export const workflowsRouter = router({
|
|||
step.reminderBody || "",
|
||||
step.id,
|
||||
step.template,
|
||||
step.sender || SENDER_ID
|
||||
step.sender || SENDER_ID,
|
||||
user.id
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -555,6 +561,7 @@ export const workflowsRouter = router({
|
|||
template: newStep.template,
|
||||
numberRequired: newStep.numberRequired,
|
||||
sender: newStep.sender || SENDER_ID,
|
||||
numberVerificationPending: false,
|
||||
},
|
||||
});
|
||||
//cancel all reminders of step and create new ones (not for newEventTypes)
|
||||
|
@ -666,7 +673,8 @@ export const workflowsRouter = router({
|
|||
newStep.reminderBody || "",
|
||||
newStep.id || 0,
|
||||
newStep.template,
|
||||
newStep.sender || SENDER_ID
|
||||
newStep.sender || SENDER_ID,
|
||||
user.id
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -695,7 +703,7 @@ export const workflowsRouter = router({
|
|||
const newStep = step;
|
||||
newStep.sender = step.sender || SENDER_ID;
|
||||
const createdStep = await ctx.prisma.workflowStep.create({
|
||||
data: step,
|
||||
data: { ...step, numberVerificationPending: false },
|
||||
});
|
||||
if (
|
||||
(trigger === WorkflowTriggerEvents.BEFORE_EVENT ||
|
||||
|
@ -782,7 +790,8 @@ export const workflowsRouter = router({
|
|||
step.reminderBody || "",
|
||||
createdStep.id,
|
||||
step.template,
|
||||
step.sender || SENDER_ID
|
||||
step.sender || SENDER_ID,
|
||||
user.id
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -848,6 +857,18 @@ export const workflowsRouter = router({
|
|||
|
||||
const senderID = sender || SENDER_ID;
|
||||
|
||||
if (action === WorkflowActions.SMS_NUMBER) {
|
||||
if (!sendTo) throw new TRPCError({ code: "BAD_REQUEST", message: "Missing sendTo" });
|
||||
const verifiedNumbers = await ctx.prisma.verifiedNumber.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
phoneNumber: sendTo,
|
||||
},
|
||||
});
|
||||
if (!verifiedNumbers)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "Phone number is not verified" });
|
||||
}
|
||||
|
||||
try {
|
||||
const userWorkflow = await ctx.prisma.workflow.findUnique({
|
||||
where: {
|
||||
|
@ -953,7 +974,8 @@ export const workflowsRouter = router({
|
|||
reminderBody,
|
||||
0,
|
||||
template,
|
||||
senderID
|
||||
senderID,
|
||||
ctx.user.id
|
||||
);
|
||||
return { message: "Notification sent" };
|
||||
}
|
||||
|
@ -1031,6 +1053,39 @@ export const workflowsRouter = router({
|
|||
});
|
||||
}
|
||||
}),
|
||||
sendVerificationCode: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
phoneNumber: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input }) => {
|
||||
const { phoneNumber } = input;
|
||||
return sendVerificationCode(phoneNumber);
|
||||
}),
|
||||
verifyPhoneNumber: authedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
phoneNumber: z.string(),
|
||||
code: z.string(),
|
||||
})
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const { phoneNumber, code } = input;
|
||||
const { user } = ctx;
|
||||
const verifyStatus = await verifyPhoneNumber(phoneNumber, code, user.id);
|
||||
return verifyStatus;
|
||||
}),
|
||||
getVerifiedNumbers: authedProcedure.query(async ({ ctx }) => {
|
||||
const { user } = ctx;
|
||||
const verifiedNumbers = await ctx.prisma.verifiedNumber.findMany({
|
||||
where: {
|
||||
userId: user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return verifiedNumbers;
|
||||
}),
|
||||
getWorkflowActionOptions: authedProcedure.query(async ({ ctx }) => {
|
||||
const userId = ctx.user.id;
|
||||
const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId } })) > 0;
|
||||
|
|
|
@ -9,15 +9,22 @@ export type PhoneInputProps<FormValues> = Props<
|
|||
required: boolean;
|
||||
},
|
||||
FormValues
|
||||
>;
|
||||
> & { onChange?: (e: any) => void };
|
||||
|
||||
function PhoneInput<FormValues>({ control, name, className, ...rest }: PhoneInputProps<FormValues>) {
|
||||
function PhoneInput<FormValues>({
|
||||
control,
|
||||
name,
|
||||
className,
|
||||
onChange,
|
||||
...rest
|
||||
}: PhoneInputProps<FormValues>) {
|
||||
return (
|
||||
<BasePhoneInput
|
||||
{...rest}
|
||||
international
|
||||
name={name}
|
||||
control={control}
|
||||
onChange={onChange}
|
||||
countrySelectProps={{ className: "text-black" }}
|
||||
numberInputProps={{
|
||||
className: "border-0 text-sm focus:ring-0 dark:bg-darkgray-100 dark:placeholder:text-darkgray-600",
|
||||
|
|
|
@ -184,6 +184,7 @@
|
|||
"$TWILIO_SID",
|
||||
"$TWILIO_MESSAGING_SID",
|
||||
"$TWILIO_PHONE_NUMBER",
|
||||
"$TWILIO_VERIFY_SID",
|
||||
"$CRON_API_KEY",
|
||||
"$DAILY_API_KEY",
|
||||
"$DAILY_SCALE_PLAN",
|
||||
|
|
Loading…
Reference in New Issue
Block a user