diff --git a/apps/web/components/booking/pages/BookingPage.tsx b/apps/web/components/booking/pages/BookingPage.tsx index 799e2824..15b291cd 100644 --- a/apps/web/components/booking/pages/BookingPage.tsx +++ b/apps/web/components/booking/pages/BookingPage.tsx @@ -66,6 +66,7 @@ type BookingFormValues = { locationType?: LocationType; guests?: string[]; phone?: string; + hostPhoneNumber?: string; // Maybe come up with a better way to name this to distingish between two types of phone numbers customInputs?: { [key: string]: string; }; @@ -193,7 +194,7 @@ const BookingPage = ({ const eventTypeDetail = { isWeb3Active: false, ...eventType }; - type Location = { type: LocationType; address?: string; link?: string }; + type Location = { type: LocationType; address?: string; link?: string; hostPhoneNumber?: string }; // it would be nice if Prisma at some point in the future allowed for Json; as of now this is not the case. const locations: Location[] = useMemo( () => (eventType.locations as Location[]) || [], @@ -268,7 +269,9 @@ const BookingPage = ({ })(), }); - const getLocationValue = (booking: Pick) => { + const getLocationValue = ( + booking: Pick + ) => { const { locationType } = booking; switch (locationType) { case LocationType.Phone: { @@ -280,6 +283,9 @@ const BookingPage = ({ case LocationType.Link: { return locationInfo(locationType)?.link || ""; } + case LocationType.UserPhone: { + return locationInfo(locationType)?.hostPhoneNumber || ""; + } // Catches all other location types, such as Google Meet, Zoom etc. default: return selectedLocation || ""; diff --git a/apps/web/package.json b/apps/web/package.json index 4ef3ab48..ae1109fe 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -74,6 +74,7 @@ "ical.js": "^1.4.0", "ics": "^2.31.0", "jimp": "^0.16.1", + "libphonenumber-js": "^1.9.53", "lodash": "^4.17.21", "micro": "^9.3.4", "mime-types": "^2.1.35", diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index a275884d..e7566b6a 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -21,6 +21,7 @@ import classNames from "classnames"; import dayjs from "dayjs"; import timezone from "dayjs/plugin/timezone"; import utc from "dayjs/plugin/utc"; +import { isValidPhoneNumber, parsePhoneNumber } from "libphonenumber-js"; import { GetServerSidePropsContext } from "next"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; @@ -70,6 +71,7 @@ import CheckboxField from "@components/ui/form/CheckboxField"; import CheckedSelect from "@components/ui/form/CheckedSelect"; import { DateRangePicker } from "@components/ui/form/DateRangePicker"; import MinutesField from "@components/ui/form/MinutesField"; +import PhoneInput from "@components/ui/form/PhoneInput"; import Select from "@components/ui/form/Select"; import * as RadioArea from "@components/ui/form/radio-area"; import WebhookListContainer from "@components/webhook/WebhookListContainer"; @@ -422,6 +424,34 @@ const EventTypePage = (props: inferSSRProps) => { ); + case LocationType.UserPhone: + return ( +
+ +
+ location.type === LocationType.UserPhone)?.hostPhoneNumber + } + /> + {locationFormMethods.formState.errors.locationPhoneNumber && ( +

+ {locationFormMethods.formState.errors.locationPhoneNumber.message} +

+ )} +
+
+ ); case LocationType.Phone: return

{t("cal_invitee_phone_number_scheduling")}

; /* TODO: Render this dynamically from App Store */ @@ -509,7 +539,7 @@ const EventTypePage = (props: inferSSRProps) => { hidden: boolean; hideCalendarNotes: boolean; hashedLink: string | undefined; - locations: { type: LocationType; address?: string; link?: string }[]; + locations: { type: LocationType; address?: string; link?: string; hostPhoneNumber?: string }[]; customInputs: EventTypeCustomInput[]; users: string[]; schedule: number; @@ -542,11 +572,16 @@ const EventTypePage = (props: inferSSRProps) => { const locationFormSchema = z.object({ locationType: z.string(), locationAddress: z.string().optional(), + locationPhoneNumber: z + .string() + .refine((val) => isValidPhoneNumber(val)) + .optional(), locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field }); const locationFormMethods = useForm<{ locationType: LocationType; + locationPhoneNumber?: string; locationAddress?: string; // TODO: We should validate address or fetch the address from googles api to see if its valid? locationLink?: string; // Currently this only accepts links that are HTTPS:// }>({ @@ -565,7 +600,11 @@ const EventTypePage = (props: inferSSRProps) => { if (e?.value) { const newLocationType: LocationType = e.value; locationFormMethods.setValue("locationType", newLocationType); - if (newLocationType === LocationType.InPerson || newLocationType === LocationType.Link) { + if ( + newLocationType === LocationType.InPerson || + newLocationType === LocationType.Link || + newLocationType === LocationType.UserPhone + ) { openLocationModal(newLocationType); } else { addLocation(newLocationType); @@ -602,6 +641,16 @@ const EventTypePage = (props: inferSSRProps) => { /> )} + {location.type === LocationType.UserPhone && ( +
+ + +
+ )} {location.type === LocationType.Phone && (
@@ -869,6 +918,7 @@ const EventTypePage = (props: inferSSRProps) => { locationFormMethods.setValue("locationType", location.type); locationFormMethods.unregister("locationLink"); locationFormMethods.unregister("locationAddress"); + locationFormMethods.unregister("locationPhoneNumber"); openLocationModal(location.type); }} aria-label={t("edit")} @@ -1925,7 +1975,9 @@ const EventTypePage = (props: inferSSRProps) => { if (newLocation === LocationType.Link) { details = { link: values.locationLink }; } - + if (newLocation === LocationType.UserPhone) { + details = { hostPhoneNumber: values.locationPhoneNumber }; + } addLocation(newLocation, details); setShowLocationModal(false); }}> @@ -1946,6 +1998,7 @@ const EventTypePage = (props: inferSSRProps) => { locationFormMethods.setValue("locationType", val.value); locationFormMethods.unregister("locationLink"); locationFormMethods.unregister("locationAddress"); + locationFormMethods.unregister("locationPhoneNumber"); setSelectedLocation(val); } }} diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 97556067..af287e66 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -434,6 +434,8 @@ "link_meeting": "Link meeting", "phone_call": "Phone call", "phone_number": "Phone Number", + "attendee_phone_number": "Attendee Phone Number", + "host_phone_number": "Your Phone Number", "enter_phone_number": "Enter phone number", "reschedule": "Reschedule", "reschedule_this": "Reschedule instead", @@ -624,6 +626,7 @@ "calendar_days": "calendar days", "business_days": "business days", "set_address_place": "Set an address or place", + "set_your_phone_number": "Set a phone number for the meeting", "set_link_meeting": "Set a link to the meeting", "cal_invitee_phone_number_scheduling": "Cal will ask your invitee to enter a phone number before scheduling.", "cal_provide_google_meet_location": "Cal will provide a Google Meet location.", diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 681d52b7..6fb2adf0 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -1,6 +1,7 @@ export enum DefaultLocationType { InPerson = "inPerson", Phone = "phone", + UserPhone = "userPhone", Link = "link", } diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index 47f24cef..ebfb043a 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -36,7 +36,8 @@ function translateLocations(locations: OptionTypeBase[], t: TFunction) { const defaultLocations: OptionTypeBase[] = [ { value: LocationType.InPerson, label: "in_person_meeting" }, { value: LocationType.Link, label: "link_meeting" }, - { value: LocationType.Phone, label: "phone_call" }, + { value: LocationType.Phone, label: "attendee_phone_number" }, + { value: LocationType.UserPhone, label: "host_phone_number" }, ]; export function getLocationOptions(integrations: AppMeta, t: TFunction) { diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index dbd964b7..9148f54d 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -9,6 +9,7 @@ export const eventTypeLocations = z.array( type: z.nativeEnum(LocationType), address: z.string().optional(), link: z.string().url().optional(), + hostPhoneNumber: z.string().optional(), }) );